@adrdp/pg-rest 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3549 @@
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
+ };
153
+
154
+ //#endregion
155
+ //#region src/parse/field.ts
156
+ const parseError = (message, details) => new PgRestError(400, "PGRST100", message, details ?? null);
157
+ /**
158
+ * Scan `s` while tracking double-quote and parenthesis state, calling `visit`
159
+ * with the index of every character that is at depth 0 and outside quotes.
160
+ * Returning `true` from `visit` stops the scan.
161
+ */
162
+ const scanTopLevel = (s, visit) => {
163
+ let depth = 0;
164
+ let quoted = false;
165
+ for (let i = 0; i < s.length; i++) {
166
+ const ch = s[i];
167
+ if (quoted) {
168
+ if (ch === "\\") i++;
169
+ else if (ch === "\"") quoted = false;
170
+ continue;
171
+ }
172
+ if (ch === "\"") {
173
+ quoted = true;
174
+ continue;
175
+ }
176
+ if (ch === "(") {
177
+ depth++;
178
+ continue;
179
+ }
180
+ if (ch === ")") {
181
+ depth--;
182
+ continue;
183
+ }
184
+ if (depth === 0 && visit(i, ch) === true) return;
185
+ }
186
+ };
187
+ /** Split on `sep` at depth 0, outside quotes. */
188
+ const splitTopLevel = (s, sep) => {
189
+ const parts = [];
190
+ let start = 0;
191
+ scanTopLevel(s, (i, ch) => {
192
+ if (ch === sep) {
193
+ parts.push(s.slice(start, i));
194
+ start = i + 1;
195
+ }
196
+ });
197
+ parts.push(s.slice(start));
198
+ return parts;
199
+ };
200
+ const unquote = (s) => {
201
+ const t = s.trim();
202
+ if (t.length >= 2 && t.startsWith("\"") && t.endsWith("\"")) return t.slice(1, -1).replace(/\\(["\\])/g, "$1");
203
+ return t;
204
+ };
205
+ /** Parse `col`, `"odd col"`, `col->a->>b`, `col->0`. */
206
+ const parseField = (input) => {
207
+ const s = input.trim();
208
+ if (!s) throw parseError("Empty column name");
209
+ const tokens = [];
210
+ const arrows = [];
211
+ let start = 0;
212
+ let quoted = false;
213
+ for (let i = 0; i < s.length; i++) {
214
+ const ch = s[i];
215
+ if (quoted) {
216
+ if (ch === "\\") i++;
217
+ else if (ch === "\"") quoted = false;
218
+ continue;
219
+ }
220
+ if (ch === "\"") quoted = true;
221
+ else if (ch === "-" && s[i + 1] === ">") {
222
+ const long = s[i + 2] === ">";
223
+ tokens.push(s.slice(start, i));
224
+ arrows.push(long ? "->>" : "->");
225
+ i += long ? 2 : 1;
226
+ start = i + 1;
227
+ }
228
+ }
229
+ tokens.push(s.slice(start));
230
+ const name = unquote(tokens[0]);
231
+ if (!name) throw parseError("Empty column name", `in "${input}"`);
232
+ const json = [];
233
+ for (let i = 1; i < tokens.length; i++) {
234
+ const raw = tokens[i].trim();
235
+ if (!raw) throw parseError("Invalid JSON path", `in "${input}"`);
236
+ const isIndex = /^-?\d+$/.test(raw);
237
+ json.push({
238
+ arrow: arrows[i - 1],
239
+ key: isIndex ? raw : unquote(raw),
240
+ isIndex
241
+ });
242
+ }
243
+ return {
244
+ name,
245
+ json
246
+ };
247
+ };
248
+
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
+ //#endregion
281
+ //#region src/parse/select.ts
282
+ const AGGREGATES = new Set([
283
+ "count",
284
+ "sum",
285
+ "avg",
286
+ "max",
287
+ "min"
288
+ ]);
289
+ /** Output key of a field in the JSON result. */
290
+ const outputName = (item) => {
291
+ if (item.alias) return item.alias;
292
+ if (item.kind === "aggregate") return item.fn;
293
+ const last = item.field.json[item.field.json.length - 1];
294
+ return last ? last.key : item.field.name;
295
+ };
296
+ const parseItem = (raw) => {
297
+ let s = raw.trim();
298
+ if (!s) throw parseError("Empty item in select", `in "${raw}"`);
299
+ if (s === "*") return { kind: "star" };
300
+ let spread = false;
301
+ if (s.startsWith("...")) {
302
+ spread = true;
303
+ s = s.slice(3);
304
+ }
305
+ let cast;
306
+ let castAt = -1;
307
+ scanTopLevel(s, (i, ch) => {
308
+ if (ch === ":" && s[i + 1] === ":") castAt = i;
309
+ });
310
+ if (castAt >= 0) {
311
+ cast = safeType(s.slice(castAt + 2));
312
+ s = s.slice(0, castAt);
313
+ }
314
+ let alias;
315
+ let aliasAt = -1;
316
+ scanTopLevel(s, (i, ch) => {
317
+ if (ch === ":") {
318
+ aliasAt = i;
319
+ return true;
320
+ }
321
+ return false;
322
+ });
323
+ if (aliasAt >= 0) {
324
+ alias = unquote(s.slice(0, aliasAt));
325
+ s = s.slice(aliasAt + 1).trim();
326
+ if (!alias) throw parseError("Empty alias in select", `in "${raw}"`);
327
+ }
328
+ const parenAt = firstParen(s);
329
+ if (parenAt >= 0) {
330
+ if (!s.endsWith(")")) throw parseError("Unbalanced parentheses in select", `in "${raw}"`);
331
+ const head = s.slice(0, parenAt).trim();
332
+ const inner = s.slice(parenAt + 1, -1);
333
+ if (inner.trim() === "") {
334
+ const dot = head.lastIndexOf(".");
335
+ const fn = (dot >= 0 ? head.slice(dot + 1) : head).trim().toLowerCase();
336
+ const colPart = dot >= 0 ? head.slice(0, dot) : "";
337
+ if (AGGREGATES.has(fn) && (colPart || fn === "count")) {
338
+ if (spread) throw parseError("Cannot spread an aggregate", `in "${raw}"`);
339
+ return {
340
+ kind: "aggregate",
341
+ fn,
342
+ ...colPart ? { field: parseField(colPart) } : {},
343
+ ...alias ? { alias } : {},
344
+ ...cast ? { cast } : {}
345
+ };
346
+ }
347
+ }
348
+ if (cast) throw parseError("Cannot cast an embedded resource", `in "${raw}"`);
349
+ const [targetRaw, ...mods] = splitTopLevel(head, "!");
350
+ const target = unquote(targetRaw ?? "");
351
+ if (!target) throw parseError("Empty embedded resource name", `in "${raw}"`);
352
+ let join$1 = "left";
353
+ let hint;
354
+ for (const m of mods) {
355
+ const mod = unquote(m);
356
+ if (mod === "inner") join$1 = "inner";
357
+ else if (mod === "left") join$1 = "left";
358
+ else if (hint === void 0) hint = mod;
359
+ else throw parseError("Only one hint is allowed per embedded resource", `in "${raw}"`);
360
+ }
361
+ return {
362
+ kind: "embed",
363
+ target,
364
+ ...alias ? { alias } : {},
365
+ ...hint ? { hint } : {},
366
+ join: join$1,
367
+ spread,
368
+ items: inner.trim() === "" ? [] : parseSelect(inner)
369
+ };
370
+ }
371
+ if (spread) throw parseError("Only embedded resources can be spread", `in "${raw}"`);
372
+ return {
373
+ kind: "field",
374
+ field: parseField(s),
375
+ ...alias ? { alias } : {},
376
+ ...cast ? { cast } : {}
377
+ };
378
+ };
379
+ const firstParen = (s) => {
380
+ let quoted = false;
381
+ for (let i = 0; i < s.length; i++) {
382
+ const ch = s[i];
383
+ if (quoted) {
384
+ if (ch === "\\") i++;
385
+ else if (ch === "\"") quoted = false;
386
+ } else if (ch === "\"") quoted = true;
387
+ else if (ch === "(") return i;
388
+ }
389
+ return -1;
390
+ };
391
+ /** Parse the value of the `select` query parameter. */
392
+ const parseSelect = (input) => {
393
+ if (input.trim() === "") return [{ kind: "star" }];
394
+ return splitTopLevel(input, ",").map(parseItem);
395
+ };
396
+
397
+ //#endregion
398
+ //#region src/parse/parsec.ts
399
+ /**
400
+ * A tiny port of Haskell's Parsec (the semantics of consumed/empty results, `try`, `<|>`, labels and error
401
+ * merging), just enough to run PostgREST's parsers and produce the same error messages.
402
+ */
403
+ /** Message kinds, in Parsec's sort order. */
404
+ const SYS_UNEXPECT = 0;
405
+ const UNEXPECT = 1;
406
+ const EXPECT = 2;
407
+ const MESSAGE = 3;
408
+ const unknown = (pos) => ({
409
+ pos,
410
+ msgs: []
411
+ });
412
+ const mergeError = (e1, e2) => {
413
+ if (e2.msgs.length === 0 && e1.msgs.length > 0) return e1;
414
+ if (e1.msgs.length === 0 && e2.msgs.length > 0) return e2;
415
+ if (e1.pos === e2.pos) return {
416
+ pos: e1.pos,
417
+ msgs: [...e1.msgs, ...e2.msgs]
418
+ };
419
+ return e1.pos > e2.pos ? e1 : e2;
420
+ };
421
+ const setExpect = (e, label$1) => ({
422
+ pos: e.pos,
423
+ msgs: [...e.msgs.filter((m) => m.k !== EXPECT), ...label$1 ? [{
424
+ k: EXPECT,
425
+ s: label$1
426
+ }] : []]
427
+ });
428
+ const showChar = (c) => `'${c}'`;
429
+ /** Haskell `show` of a one-character string. */
430
+ const showStr1 = (c) => {
431
+ if (c === "\"") return "\"\\\"\"";
432
+ if (c === "\\") return "\"\\\\\"";
433
+ if (c === "\n") return "\"\\n\"";
434
+ if (c === " ") return "\"\\t\"";
435
+ const code = c.codePointAt(0);
436
+ return code > 126 || code < 32 ? `"\\${code}"` : `"${c}"`;
437
+ };
438
+ const showStr = (s) => s.length === 1 ? showStr1(s) : `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
439
+ const pure = (v) => (_s, i) => ({
440
+ ok: true,
441
+ consumed: false,
442
+ v,
443
+ i,
444
+ err: unknown(i)
445
+ });
446
+ const zero = (_s, i) => ({
447
+ ok: false,
448
+ consumed: false,
449
+ err: unknown(i)
450
+ });
451
+ const unexpected = (msg) => (_s, i) => ({
452
+ ok: false,
453
+ consumed: false,
454
+ err: {
455
+ pos: i,
456
+ msgs: [{
457
+ k: UNEXPECT,
458
+ s: msg
459
+ }]
460
+ }
461
+ });
462
+ const codePointAt = (s, i) => {
463
+ if (i >= s.length) return void 0;
464
+ const cp = s.codePointAt(i);
465
+ return String.fromCodePoint(cp);
466
+ };
467
+ /** Consume one character satisfying `f`. */
468
+ const satisfy = (f) => (s, i) => {
469
+ const c = codePointAt(s, i);
470
+ if (c === void 0) return {
471
+ ok: false,
472
+ consumed: false,
473
+ err: {
474
+ pos: i,
475
+ msgs: [{
476
+ k: SYS_UNEXPECT,
477
+ s: ""
478
+ }]
479
+ }
480
+ };
481
+ if (!f(c)) return {
482
+ ok: false,
483
+ consumed: false,
484
+ err: {
485
+ pos: i,
486
+ msgs: [{
487
+ k: SYS_UNEXPECT,
488
+ s: showStr1(c)
489
+ }]
490
+ }
491
+ };
492
+ const j = i + c.length;
493
+ return {
494
+ ok: true,
495
+ consumed: true,
496
+ v: c,
497
+ i: j,
498
+ err: unknown(j)
499
+ };
500
+ };
501
+ const label = (p, name) => (s, i) => {
502
+ const r = p(s, i);
503
+ if (r.consumed) return r;
504
+ if (r.ok) return {
505
+ ...r,
506
+ err: r.err.msgs.length === 0 ? r.err : setExpect(r.err, name)
507
+ };
508
+ return {
509
+ ...r,
510
+ err: setExpect(r.err, name)
511
+ };
512
+ };
513
+ const char = (c) => label(satisfy((x) => x === c), showStr1(c));
514
+ const oneOf = (cs) => satisfy((x) => cs.includes(x));
515
+ const noneOf = (cs) => satisfy((x) => !cs.includes(x));
516
+ const anyChar = satisfy(() => true);
517
+ const letter = label(satisfy((c) => /\p{L}/u.test(c)), "letter");
518
+ const digit = label(satisfy((c) => c >= "0" && c <= "9"), "digit");
519
+ /** Parsec's `string`: fails after consuming when only a prefix matches (wrap in `tryP` to backtrack). */
520
+ const string = (w) => (s, i) => {
521
+ const expect = {
522
+ k: EXPECT,
523
+ s: showStr(w)
524
+ };
525
+ for (let k = 0; k < w.length; k++) {
526
+ const c = codePointAt(s, i + k);
527
+ if (c === void 0) return {
528
+ ok: false,
529
+ consumed: k > 0,
530
+ err: {
531
+ pos: i,
532
+ msgs: [{
533
+ k: SYS_UNEXPECT,
534
+ s: ""
535
+ }, expect]
536
+ }
537
+ };
538
+ if (c !== w[k]) {
539
+ const err = {
540
+ pos: i,
541
+ msgs: [{
542
+ k: SYS_UNEXPECT,
543
+ s: showStr1(c)
544
+ }, expect]
545
+ };
546
+ return {
547
+ ok: false,
548
+ consumed: k > 0,
549
+ err
550
+ };
551
+ }
552
+ }
553
+ const j = i + w.length;
554
+ return {
555
+ ok: true,
556
+ consumed: w.length > 0,
557
+ v: w,
558
+ i: j,
559
+ err: unknown(j)
560
+ };
561
+ };
562
+ const tryP = (p) => (s, i) => {
563
+ const r = p(s, i);
564
+ return !r.ok && r.consumed ? {
565
+ ok: false,
566
+ consumed: false,
567
+ err: r.err
568
+ } : r;
569
+ };
570
+ const bind = (p, f) => (s, i) => {
571
+ const r = p(s, i);
572
+ if (!r.ok) return r;
573
+ const q = f(r.v)(s, r.i);
574
+ if (r.consumed) {
575
+ if (q.consumed) return q;
576
+ return q.ok ? {
577
+ ...q,
578
+ consumed: true,
579
+ err: mergeError(r.err, q.err)
580
+ } : {
581
+ ok: false,
582
+ consumed: true,
583
+ err: mergeError(r.err, q.err)
584
+ };
585
+ }
586
+ if (q.consumed) return q;
587
+ return q.ok ? {
588
+ ...q,
589
+ err: mergeError(r.err, q.err)
590
+ } : {
591
+ ok: false,
592
+ consumed: false,
593
+ err: mergeError(r.err, q.err)
594
+ };
595
+ };
596
+ const map = (p, f) => bind(p, (a) => pure(f(a)));
597
+ /** `p <* q` */
598
+ const before = (p, q) => bind(p, (a) => map(q, () => a));
599
+ /** `p *> q` */
600
+ const after = (p, q) => bind(p, () => q);
601
+ const alt = (...ps) => (s, i) => {
602
+ let acc;
603
+ for (const p of ps) {
604
+ const r = p(s, i);
605
+ if (r.consumed) return r;
606
+ if (r.ok) return acc ? {
607
+ ...r,
608
+ err: mergeError(acc, r.err)
609
+ } : r;
610
+ acc = acc ? mergeError(acc, r.err) : r.err;
611
+ }
612
+ return {
613
+ ok: false,
614
+ consumed: false,
615
+ err: acc ?? unknown(i)
616
+ };
617
+ };
618
+ const many = (p) => (s, i) => {
619
+ const out = [];
620
+ let consumed = false;
621
+ let pos = i;
622
+ for (;;) {
623
+ const r = p(s, pos);
624
+ if (!r.ok) {
625
+ if (r.consumed) return r;
626
+ return {
627
+ ok: true,
628
+ consumed,
629
+ v: out,
630
+ i: pos,
631
+ err: r.err
632
+ };
633
+ }
634
+ if (!r.consumed) throw new Error("combinator 'many' is applied to a parser that accepts an empty string");
635
+ out.push(r.v);
636
+ consumed = true;
637
+ pos = r.i;
638
+ }
639
+ };
640
+ const many1 = (p) => bind(p, (x) => map(many(p), (xs) => [x, ...xs]));
641
+ const sepBy1 = (p, sep) => bind(p, (x) => map(many(after(sep, p)), (xs) => [x, ...xs]));
642
+ const sepBy = (p, sep) => alt(sepBy1(p, sep), pure([]));
643
+ const option = (d, p) => alt(p, pure(d));
644
+ const optionMaybe = (p) => option(void 0, p);
645
+ const between = (open, close, p) => after(open, before(p, close));
646
+ const lookAhead = (p) => (s, i) => {
647
+ const r = p(s, i);
648
+ if (!r.ok) return r;
649
+ return {
650
+ ok: true,
651
+ consumed: false,
652
+ v: r.v,
653
+ i,
654
+ err: unknown(i)
655
+ };
656
+ };
657
+ const notFollowedBy = (p) => tryP(alt(bind(tryP(p), (c) => unexpected(typeof c === "string" && c.length === 1 ? showChar(c) : showStr(String(c)))), pure(void 0)));
658
+ /** `eof` does not move the error position past the offending character (Parsec's `anyToken` keeps the position). */
659
+ const eof = label((s, i) => {
660
+ const c = codePointAt(s, i);
661
+ if (c === void 0) return {
662
+ ok: true,
663
+ consumed: false,
664
+ v: void 0,
665
+ i,
666
+ err: unknown(i)
667
+ };
668
+ return {
669
+ ok: false,
670
+ consumed: false,
671
+ err: {
672
+ pos: i,
673
+ msgs: [{
674
+ k: UNEXPECT,
675
+ s: showChar(c)
676
+ }]
677
+ }
678
+ };
679
+ }, "end of input");
680
+ const guard = (ok) => ok ? pure(void 0) : zero;
681
+ const columnAt = (s, offset) => {
682
+ let col = 1;
683
+ for (let k = 0; k < offset; k++) col = s[k] === " " ? col + 8 - (col - 1) % 8 : col + 1;
684
+ return col;
685
+ };
686
+ const clean = (xs) => [...new Set(xs.filter((x) => x !== ""))];
687
+ const commasOr = (xs) => xs.length === 0 ? "" : xs.length === 1 ? xs[0] : `${xs.slice(0, -1).join(", ")} or ${xs[xs.length - 1]}`;
688
+ /** Parsec's `showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input"`. */
689
+ const showMessages = (msgs) => {
690
+ if (msgs.length === 0) return "unknown parse error";
691
+ const sorted = [...msgs].sort((a, b) => a.k - b.k);
692
+ const sys = sorted.filter((m) => m.k === SYS_UNEXPECT);
693
+ const unex = sorted.filter((m) => m.k === UNEXPECT);
694
+ const expect = sorted.filter((m) => m.k === EXPECT);
695
+ const other = sorted.filter((m) => m.k === MESSAGE);
696
+ const showMany = (pre, ms) => {
697
+ const c = commasOr(clean(ms.map((m) => m.s)));
698
+ return c === "" ? "" : pre ? `${pre} ${c}` : c;
699
+ };
700
+ return clean([
701
+ unex.length > 0 || sys.length === 0 ? "" : sys[0].s === "" ? "unexpected end of input" : `unexpected ${sys[0].s}`,
702
+ showMany("unexpected", unex),
703
+ showMany("expecting", expect),
704
+ showMany("", other)
705
+ ]).join(" ");
706
+ };
707
+ /** Run `p` at the start of `input` (no `eof` is implied, as in Parsec's `parse`). */
708
+ const run$1 = (p, input) => {
709
+ const r = p(input, 0);
710
+ if (r.ok) return {
711
+ ok: true,
712
+ value: r.v,
713
+ end: r.i
714
+ };
715
+ return {
716
+ ok: false,
717
+ failure: {
718
+ column: columnAt(input, r.err.pos),
719
+ details: showMessages(r.err.msgs).trim()
720
+ }
721
+ };
722
+ };
723
+
724
+ //#endregion
725
+ //#region src/parse/grammar.ts
726
+ /**
727
+ * PostgREST v16's query-string parsers (`PostgREST.ApiRequest.QueryParams`), ported on top of `parsec.ts`
728
+ * so that syntax errors read exactly like PostgREST's. They only validate: the structures the rest of the
729
+ * library works with come from the hand-written parsers in this directory.
730
+ */
731
+ const ws = many(oneOf(" "));
732
+ const lexeme = (p) => before(after(ws, p), ws);
733
+ const lazy = (f) => (s, i) => f()(s, i);
734
+ const join = (xs) => xs.join("");
735
+ const identifier = map(many1(alt(letter, digit, oneOf("_ $"))), (cs) => join(cs).trim());
736
+ const dash = tryP(after(char("-"), notFollowedBy(char(">"))));
737
+ const sepByDash = (p) => map(sepBy1(p, dash), (xs) => xs.join("-"));
738
+ const slashedOrPlain = alt(noneOf("\\\""), after(char("\\"), anyChar));
739
+ const quotedValue = map(between(char("\""), char("\""), many(slashedOrPlain)), join);
740
+ const fieldName = label(alt(quotedValue, sepByDash(identifier)), "field name (* or [a..z0..9_$])");
741
+ const jsonKeyName = label(alt(quotedValue, sepByDash(map(many1(noneOf("(-:.,>)")), (cs) => join(cs).trim()))), "any non reserved character different from: .,>()");
742
+ const delimiter = label(char("."), "delimiter (.)");
743
+ const jsonEnd = alt(tryP(lookAhead(string("->"))), tryP(lookAhead(string("::"))), tryP(lookAhead(string("."))), tryP(lookAhead(string(","))), tryP(eof));
744
+ const jsonIdx = before(bind(option("+", char("-")), () => many1(digit)), jsonEnd);
745
+ const jsonPath = many(after(alt(tryP(string("->>")), tryP(string("->"))), alt(tryP(jsonIdx), tryP(jsonKeyName))));
746
+ const field = lexeme(bind(fieldName, () => option([], jsonPath)));
747
+ const aliasPrefix = optionMaybe(tryP(before(fieldName, after(char(":"), notFollowedBy(char(":"))))));
748
+ const embedParam = after(char("!"), alt(tryP(string("left")), tryP(string("inner")), tryP(fieldName)));
749
+ const embedParams = after(optionMaybe(embedParam), optionMaybe(embedParam));
750
+ const selectEnd = alt(tryP(lookAhead(string(")"))), tryP(lookAhead(string(","))), tryP(eof));
751
+ const aggregation = alt(string("sum"), string("avg"), string("count"), tryP(string("max")), tryP(string("min")), zero);
752
+ const relationSelect = lexeme(bind(aliasPrefix, () => bind(fieldName, (name) => after(guard(name !== "count"), after(embedParams, tryP(lookAhead(string("("))))))));
753
+ const spreadRelationSelect = lexeme(after(string("..."), after(fieldName, after(embedParams, tryP(lookAhead(string("(")))))));
754
+ const fieldSelect = lexeme(alt(tryP(after(string("*"), selectEnd)), tryP(after(aliasPrefix, after(string("count()"), after(optionMaybe(after(string("::"), identifier)), selectEnd)))), after(aliasPrefix, after(field, after(optionMaybe(after(string("::"), identifier)), after(optionMaybe(tryP(before(after(char("."), aggregation), string("()")))), after(optionMaybe(after(string("::"), identifier)), selectEnd)))))));
755
+ const forest = sepBy(alt(bind(tryP(spreadRelationSelect), () => between(char("("), char(")"), lazy(() => forest))), bind(tryP(relationSelect), () => between(char("("), char(")"), lazy(() => forest))), fieldSelect), lexeme(char(",")));
756
+ const ciChar = (c) => alt(char(c), char(c.toUpperCase()));
757
+ const ciString = (w) => map(seq([...w].map(ciChar)), join);
758
+ const seq = (ps) => ps.reduce((acc, p) => bind(acc, (xs) => map(p, (x) => [...xs, x])), pure([]));
759
+ const listElement = alt(tryP(before(quotedValue, notFollowedBy(noneOf(",)")))), map(many(noneOf(",)")), join));
760
+ const listVal = before(after(lexeme(char("(")), sepBy1(listElement, char(","))), lexeme(char(")")));
761
+ const isVal = label(alt(tryP(ciString("null")), tryP(ciString("not_null")), tryP(ciString("true")), tryP(ciString("false")), tryP(ciString("unknown"))), "isVal: (null, not_null, true, false, unknown)");
762
+ const simpleOperator = label(alt(...[
763
+ "neq",
764
+ "cs",
765
+ "cd",
766
+ "ov",
767
+ "sl",
768
+ "sr",
769
+ "nxr",
770
+ "nxl",
771
+ "adj"
772
+ ].map((o) => tryP(string(o)))), "unknown single value operator");
773
+ const quantOperator = label(alt(...[
774
+ "eq",
775
+ "gte",
776
+ "gt",
777
+ "lte",
778
+ "lt",
779
+ "like",
780
+ "ilike",
781
+ "match",
782
+ "imatch"
783
+ ].map((o) => tryP(string(o)))), "unknown single value operator");
784
+ const opExpr = (singleVal$1) => {
785
+ const pIn = after(tryP(after(string("in"), delimiter)), listVal);
786
+ const pIs = after(tryP(after(string("is"), delimiter)), isVal);
787
+ const pIsDist = after(tryP(after(string("isdistinct"), delimiter)), singleVal$1);
788
+ const pSimple = bind(simpleOperator, () => after(delimiter, singleVal$1));
789
+ const pQuant = bind(quantOperator, () => after(optionMaybe(tryP(between(char("("), char(")"), alt(tryP(string("any")), string("all"))))), after(delimiter, singleVal$1)));
790
+ const operation = label(alt(pIn, pIs, pIsDist, tryP(bind(alt(...[
791
+ "fts",
792
+ "plfts",
793
+ "phfts",
794
+ "wfts"
795
+ ].map((o) => tryP(string(o)))), () => after(optionMaybe(tryP(between(char("("), char(")"), identifier))), after(delimiter, singleVal$1)))), tryP(pSimple), tryP(pQuant)), "operator (eq, gt, ...)");
796
+ return after(alt(tryP(after(string("not"), delimiter)), pure("")), operation);
797
+ };
798
+ const singleVal = map(many(anyChar), join);
799
+ const orderEnd = alt(tryP(lookAhead(char(","))), tryP(eof));
800
+ const orderNulls = alt(tryP(after(delimiter, string("nullsfirst"))), tryP(after(delimiter, string("nullslast"))));
801
+ const orderTail = after(optionMaybe(alt(tryP(after(delimiter, string("asc"))), tryP(after(delimiter, string("desc"))))), alt(before(optionMaybe(orderNulls), orderEnd), after(orderEnd, pure(void 0))));
802
+ const orderTerm = after(field, orderTail);
803
+ const order = sepBy1(lexeme(alt(tryP(after(fieldName, after(between(char("("), char(")"), field), orderTail))), orderTerm)), char(","));
804
+ const pgArray = map(bind(string("{"), (a) => bind(many(noneOf("{}")), (b) => map(string("}"), (c) => a + join(b) + c))), (x) => x);
805
+ const logicSingleVal = alt(tryP(before(quotedValue, notFollowedBy(noneOf(",)")))), tryP(pgArray), map(many(noneOf(",)")), join));
806
+ const logicNot = label(alt(tryP(after(string("not"), after(delimiter, pure(true)))), pure(false)), "negation operator (not)");
807
+ const logicOp = label(alt(tryP(string("and")), string("or")), "logic operator (and, or)");
808
+ const logicTree = alt(tryP(after(field, after(delimiter, opExpr(logicSingleVal)))), bind(logicNot, () => bind(logicOp, () => after(lexeme(char("(")), before(sepBy1(lazy(() => logicTree), lexeme(char(","))), lexeme(char(")")))))));
809
+ const fail = (what, input, f) => new PgRestError(400, "PGRST100", `"failed to parse ${what} (${input})" (line 1, column ${f.column})`, f.details);
810
+ /** `select=`: returns where the parse stopped (PostgREST ignores anything after it), or the error. */
811
+ const checkSelect = (input) => {
812
+ const r = run$1(forest, input);
813
+ return r.ok ? r.end : fail("select parameter", input, r.failure);
814
+ };
815
+ /** The right-hand side of a filter (`not.eq.1`); `undefined` when it parses. */
816
+ const checkFilter = (input) => {
817
+ const r = run$1(opExpr(singleVal), input);
818
+ return r.ok ? void 0 : fail("filter", input, r.failure);
819
+ };
820
+ /** An `order=` value; `undefined` when it parses. */
821
+ const checkOrder = (input) => {
822
+ const r = run$1(order, input);
823
+ return r.ok ? void 0 : fail("order", input, r.failure);
824
+ };
825
+ /** `and=`/`or=`: `op` is `and`, `or`, `not.and` or `not.or`; returns how much of `value` the tree uses. */
826
+ const checkLogicTree = (op, value) => {
827
+ const r = run$1(logicTree, op + value);
828
+ return r.ok ? Math.max(0, r.end - op.length) : fail("logic tree", value, r.failure);
829
+ };
830
+
831
+ //#endregion
832
+ //#region src/parse/syntax.ts
833
+ /** Helpers for the lenient parts of PostgREST's filter syntax (the error wording lives in `grammar.ts`). */
834
+ const IS_VALUES_ORDER = [
835
+ "null",
836
+ "not_null",
837
+ "true",
838
+ "false",
839
+ "unknown"
840
+ ];
841
+ const at = (s, i) => i < s.length ? s[i] : void 0;
842
+ /**
843
+ * Where a parenthesised `in` list ends (offset after the closing `)`). PostgREST stops there and ignores what follows;
844
+ * an unterminated list returns the input length (the grammar reports the error before this is used).
845
+ */
846
+ const scanList = (s, from) => {
847
+ let i = from + 1;
848
+ for (;;) {
849
+ let end = i;
850
+ if (at(s, i) === "\"") {
851
+ let k = i + 1;
852
+ while (k < s.length && s[k] !== "\"") k += s[k] === "\\" ? 2 : 1;
853
+ if (k < s.length && (at(s, k + 1) === "," || at(s, k + 1) === ")")) end = k + 1;
854
+ }
855
+ if (end === i) while (end < s.length && s[end] !== "," && s[end] !== ")") end++;
856
+ const c = at(s, end);
857
+ if (c === ")") return end + 1;
858
+ if (c !== ",") return s.length;
859
+ i = end + 1;
860
+ }
861
+ };
862
+
863
+ //#endregion
864
+ //#region src/parse/filter.ts
865
+ const OPERATORS = new Set([
866
+ "eq",
867
+ "neq",
868
+ "gt",
869
+ "gte",
870
+ "lt",
871
+ "lte",
872
+ "like",
873
+ "ilike",
874
+ "match",
875
+ "imatch",
876
+ "in",
877
+ "is",
878
+ "isdistinct",
879
+ "fts",
880
+ "plfts",
881
+ "phfts",
882
+ "wfts",
883
+ "cs",
884
+ "cd",
885
+ "ov",
886
+ "sl",
887
+ "sr",
888
+ "nxl",
889
+ "nxr",
890
+ "adj"
891
+ ]);
892
+ const QUANTIFIABLE = new Set([
893
+ "eq",
894
+ "gt",
895
+ "gte",
896
+ "lt",
897
+ "lte",
898
+ "like",
899
+ "ilike",
900
+ "match",
901
+ "imatch"
902
+ ]);
903
+ const FTS = new Set([
904
+ "fts",
905
+ "plfts",
906
+ "phfts",
907
+ "wfts"
908
+ ]);
909
+ const IS_VALUES = new Set([
910
+ "null",
911
+ "not_null",
912
+ "true",
913
+ "false",
914
+ "unknown"
915
+ ]);
916
+ const emptyParams = () => ({
917
+ filters: [],
918
+ order: [],
919
+ presence: []
920
+ });
921
+ const parseList = (raw, where) => {
922
+ const t = raw.trim();
923
+ if (!t.startsWith("(") || !t.endsWith(")")) throw parseError(`failed to parse filter (${where})`, "expected a list in parentheses, like (a,b,c)");
924
+ const inner = t.slice(1, -1);
925
+ if (inner.trim() === "") return [];
926
+ return splitTopLevel(inner, ",").map((v) => unquote(v));
927
+ };
928
+ /**
929
+ * Parse `[not.]op[(modifier)].value`, the right-hand side of a filter.
930
+ * `unquoteValue` is true inside and()/or() trees where values may be quoted.
931
+ */
932
+ const parseOperatorExpr = (field$1, expr, unquoteValue = false) => {
933
+ if (!unquoteValue) {
934
+ const bad = checkFilter(expr);
935
+ if (bad) throw bad;
936
+ }
937
+ let rest = expr;
938
+ let negate = false;
939
+ if (rest.startsWith("not.")) {
940
+ negate = true;
941
+ rest = rest.slice(4);
942
+ }
943
+ const dot = rest.indexOf(".");
944
+ if (dot < 0) throw parseError(`failed to parse filter (${expr})`);
945
+ const opToken = rest.slice(0, dot);
946
+ const rawValue = rest.slice(dot + 1);
947
+ const m = /^([a-z]+)(?:\(([^)]*)\))?$/.exec(opToken);
948
+ if (!m || !OPERATORS.has(m[1])) throw parseError(`failed to parse filter (${expr})`, `unknown operator "${opToken}"`);
949
+ const op = m[1];
950
+ const modifier = m[2];
951
+ let quantifier;
952
+ let lang;
953
+ if (modifier !== void 0) if (QUANTIFIABLE.has(op)) {
954
+ if (modifier !== "any" && modifier !== "all") throw parseError(`failed to parse filter (${expr})`, `"${modifier}" is not a valid quantifier, use any or all`);
955
+ quantifier = modifier;
956
+ } else if (FTS.has(op)) {
957
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(modifier)) throw parseError(`failed to parse filter (${expr})`, `"${modifier}" is not a valid text search configuration`);
958
+ lang = modifier;
959
+ } else throw parseError(`failed to parse filter (${expr})`, `operator ${op} does not take a modifier`);
960
+ let value = unquoteValue ? unquote(rawValue) : rawValue;
961
+ if (op === "in") {
962
+ const end = unquoteValue ? void 0 : scanList(rawValue, 0);
963
+ value = parseList(typeof end === "number" ? rawValue.slice(0, end) : rawValue, expr);
964
+ } else if (op === "is") {
965
+ const v = ((unquoteValue ? void 0 : IS_VALUES_ORDER.find((w) => rawValue.startsWith(w))) ?? String(value)).toLowerCase();
966
+ if (!IS_VALUES.has(v)) throw parseError(`failed to parse filter (${expr})`, "is only accepts null, not_null, true, false and unknown");
967
+ value = v;
968
+ }
969
+ return {
970
+ kind: "filter",
971
+ field: field$1,
972
+ negate,
973
+ op,
974
+ ...quantifier ? { quantifier } : {},
975
+ ...lang ? { lang } : {},
976
+ value
977
+ };
978
+ };
979
+ const parseLogicItem = (raw) => {
980
+ const item = raw.trim();
981
+ const m = /^(not\.)?(and|or)\(/.exec(item);
982
+ if (m) {
983
+ if (!item.endsWith(")")) throw parseError("Unbalanced parentheses in logic tree", `in "${item}"`);
984
+ const inner = item.slice(m[0].length - 1);
985
+ return parseLogic(m[2], Boolean(m[1]), inner);
986
+ }
987
+ const dot = item.indexOf(".");
988
+ if (dot < 0) throw parseError(`failed to parse logic tree (${item})`);
989
+ return parseOperatorExpr(parseField(item.slice(0, dot)), item.slice(dot + 1), true);
990
+ };
991
+ /** Parse `(a.eq.1,or(b.eq.2,c.eq.3))` into a {@link Logic} node. */
992
+ const parseLogic = (op, negate, value) => {
993
+ const v = value.trim();
994
+ if (!v.startsWith("(") || !v.endsWith(")")) throw parseError(`failed to parse logic tree (${value})`, "expected a list in parentheses");
995
+ const inner = v.slice(1, -1);
996
+ if (inner.trim() === "") throw parseError(`failed to parse logic tree (${value})`, "empty tree");
997
+ return {
998
+ kind: "logic",
999
+ op,
1000
+ negate,
1001
+ children: splitTopLevel(inner, ",").map(parseLogicItem)
1002
+ };
1003
+ };
1004
+ const parseOrder = (input) => {
1005
+ const bad = checkOrder(input);
1006
+ if (bad) throw bad;
1007
+ return parseOrderTerms(input);
1008
+ };
1009
+ const parseOrderTerms = (input) => splitTopLevel(input, ",").map((raw) => {
1010
+ const tokens = raw.trim().split(".");
1011
+ let nulls;
1012
+ let dir = "asc";
1013
+ const last = () => tokens[tokens.length - 1]?.toLowerCase();
1014
+ if (tokens.length > 1 && (last() === "nullsfirst" || last() === "nullslast")) {
1015
+ nulls = last() === "nullsfirst" ? "first" : "last";
1016
+ tokens.pop();
1017
+ }
1018
+ if (tokens.length > 1 && (last() === "asc" || last() === "desc")) {
1019
+ dir = last();
1020
+ tokens.pop();
1021
+ }
1022
+ const expr = tokens.join(".");
1023
+ const related = /^([^()>-]+)\((.+)\)$/.exec(expr);
1024
+ if (related && /[()]/.test(related[2])) throw parseError(`"failed to parse order (${input})"`, `unexpected '(' in "${expr}"`);
1025
+ return {
1026
+ ...related ? {
1027
+ relation: unquote(related[1]),
1028
+ field: parseField(related[2])
1029
+ } : { field: parseField(expr) },
1030
+ dir,
1031
+ ...nulls ? { nulls } : {}
1032
+ };
1033
+ });
1034
+ /** PostgREST ignores a `limit`/`offset` that is not an integer; a negative limit is refused later. */
1035
+ const parseCount = (name, v) => {
1036
+ return (name === "limit" ? /^-?\d+$/.test(v) : /^\d+$/.test(v)) ? Number(v) : void 0;
1037
+ };
1038
+ const walkEmbeds = (items, prefix, known) => {
1039
+ for (const it of items) {
1040
+ if (it.kind !== "embed") continue;
1041
+ const name = it.alias ?? it.target;
1042
+ const canonical = [...prefix, name].join(".");
1043
+ known.set(canonical, canonical);
1044
+ if (it.alias) known.set([...prefix, it.target].join("."), canonical);
1045
+ walkEmbeds(it.items, [...prefix, name], known);
1046
+ }
1047
+ };
1048
+ const RESERVED_TERMINALS = new Set([
1049
+ "limit",
1050
+ "offset",
1051
+ "order",
1052
+ "and",
1053
+ "or"
1054
+ ]);
1055
+ /** Parse everything PostgREST reads from the query string, except function arguments. */
1056
+ const parseQuery = (params, opts = {}) => {
1057
+ let select = [{ kind: "star" }];
1058
+ let onConflict;
1059
+ let columns;
1060
+ for (const [k, v] of params) if (k === "select") {
1061
+ const end = checkSelect(v);
1062
+ if (end instanceof PgRestError) throw end;
1063
+ const used = v.slice(0, end).trim();
1064
+ select = used === "" ? [{ kind: "star" }] : parseSelect(used);
1065
+ } else if (k === "on_conflict") onConflict = splitTopLevel(v, ",").map(unquote).filter(Boolean);
1066
+ else if (k === "columns") columns = splitTopLevel(v, ",").map(unquote).filter(Boolean);
1067
+ const known = /* @__PURE__ */ new Map();
1068
+ walkEmbeds(select, [], known);
1069
+ const root = emptyParams();
1070
+ const embeds = /* @__PURE__ */ new Map();
1071
+ const paramsFor = (path, key) => {
1072
+ if (path.length === 0) return root;
1073
+ const joined = path.join(".");
1074
+ const canonical = known.get(joined);
1075
+ if (!canonical) throw new PgRestError(400, "PGRST108", `'${joined}' is not an embedded resource in this request`, null, `Verify that '${joined}' is included in the 'select' query parameter.`);
1076
+ let p = embeds.get(canonical);
1077
+ if (!p) {
1078
+ p = emptyParams();
1079
+ embeds.set(canonical, p);
1080
+ }
1081
+ return p;
1082
+ };
1083
+ for (const [key, value] of params) {
1084
+ if (key === "select" || key === "on_conflict" || key === "columns") continue;
1085
+ if (opts.allowFilterKey && !opts.allowFilterKey(key)) continue;
1086
+ const parts = key.split(".");
1087
+ const last = parts[parts.length - 1];
1088
+ if (RESERVED_TERMINALS.has(last)) {
1089
+ let path = parts.slice(0, -1);
1090
+ if (last === "and" || last === "or") {
1091
+ const negate = path[path.length - 1] === "not";
1092
+ if (negate) path = path.slice(0, -1);
1093
+ const used = checkLogicTree(negate ? `not.${last}` : last, value);
1094
+ if (used instanceof PgRestError) throw used;
1095
+ paramsFor(path, key).filters.push(parseLogic(last, negate, value.slice(0, used)));
1096
+ } else if (last === "order") paramsFor(path, key).order.push(...parseOrder(value));
1097
+ else if (last === "limit") {
1098
+ const n = parseCount("limit", value);
1099
+ if (n !== void 0) paramsFor(path, key).limit = n;
1100
+ } else {
1101
+ const n = parseCount("offset", value);
1102
+ if (n !== void 0) paramsFor(path, key).offset = n;
1103
+ }
1104
+ continue;
1105
+ }
1106
+ const embedPath = parts.join(".");
1107
+ if (known.has(embedPath)) {
1108
+ const m = /^(not\.)?is\.null$/.exec(value);
1109
+ if (!m) throw new PgRestError(400, "PGRST120", "Embedded resource filter only accepts is.null or not.is.null", null, null);
1110
+ paramsFor(parts.slice(0, -1), key).presence.push({
1111
+ embed: known.get(embedPath).split(".").pop(),
1112
+ present: Boolean(m[1])
1113
+ });
1114
+ continue;
1115
+ }
1116
+ paramsFor(parts.slice(0, -1), key).filters.push(parseOperatorExpr(parseField(last), value));
1117
+ }
1118
+ return {
1119
+ select,
1120
+ root,
1121
+ embeds,
1122
+ ...onConflict ? { onConflict } : {},
1123
+ ...columns ? { columns } : {}
1124
+ };
1125
+ };
1126
+
1127
+ //#endregion
1128
+ //#region src/parse/request.ts
1129
+ const METHODS = new Set([
1130
+ "GET",
1131
+ "HEAD",
1132
+ "POST",
1133
+ "PATCH",
1134
+ "PUT",
1135
+ "DELETE",
1136
+ "OPTIONS"
1137
+ ]);
1138
+ const isSupportedMethod = (m) => METHODS.has(m.toUpperCase());
1139
+ const parseMethod = (m) => {
1140
+ const up = m.toUpperCase();
1141
+ if (!METHODS.has(up)) throw new PgRestError(405, "PGRST117", `Unsupported HTTP method: ${m}`);
1142
+ return up;
1143
+ };
1144
+ /**
1145
+ * Route a request path. `basePath` is stripped first, so the library can be
1146
+ * mounted anywhere (`/api/rest`, ...).
1147
+ */
1148
+ const parseTarget = (pathname, basePath = "") => {
1149
+ let path = pathname;
1150
+ if (basePath && basePath !== "/") {
1151
+ const base = basePath.replace(/\/+$/, "");
1152
+ if (path === base) path = "/";
1153
+ else if (path.startsWith(`${base}/`)) path = path.slice(base.length);
1154
+ else throw new PgRestError(404, "PGRST125", `Invalid path specified in request URL`);
1155
+ }
1156
+ const segs = path.split("/").filter(Boolean).map((s) => {
1157
+ try {
1158
+ return decodeURIComponent(s);
1159
+ } catch {
1160
+ throw new PgRestError(400, "PGRST100", "Invalid URL encoding in path");
1161
+ }
1162
+ });
1163
+ if (segs.length === 0) return { type: "root" };
1164
+ if (segs[0] === "rpc") {
1165
+ if (segs.length !== 2) throw new PgRestError(404, "PGRST125", "Invalid path specified in request URL");
1166
+ return {
1167
+ type: "rpc",
1168
+ name: segs[1]
1169
+ };
1170
+ }
1171
+ if (segs.length !== 1) throw new PgRestError(404, "PGRST125", "Invalid path specified in request URL");
1172
+ return {
1173
+ type: "table",
1174
+ name: segs[0]
1175
+ };
1176
+ };
1177
+ const PREFERENCE_VALUES = {
1178
+ return: [
1179
+ "minimal",
1180
+ "headers-only",
1181
+ "representation"
1182
+ ],
1183
+ count: [
1184
+ "exact",
1185
+ "planned",
1186
+ "estimated"
1187
+ ],
1188
+ resolution: ["merge-duplicates", "ignore-duplicates"],
1189
+ missing: ["default", "null"],
1190
+ handling: ["strict", "lenient"],
1191
+ tx: ["commit", "rollback"]
1192
+ };
1193
+ /** Parse one or more `Prefer` header values (comma separated tokens). */
1194
+ const parsePrefer = (header) => {
1195
+ const out = { invalid: [] };
1196
+ const invalid = [];
1197
+ if (!header) return out;
1198
+ for (const raw of header.split(",")) {
1199
+ const token = raw.trim();
1200
+ if (!token) continue;
1201
+ const eq = token.indexOf("=");
1202
+ if (eq < 0) {
1203
+ invalid.push(token);
1204
+ continue;
1205
+ }
1206
+ const key = token.slice(0, eq).trim().toLowerCase();
1207
+ const value = token.slice(eq + 1).trim();
1208
+ if (key === "timezone") out.timezone = value;
1209
+ else if (key === "max-affected") if (/^\d+$/.test(value)) out.maxAffected = Number(value);
1210
+ else invalid.push(token);
1211
+ else if (PREFERENCE_VALUES[key]?.includes(value)) out[key] = value;
1212
+ else invalid.push(token);
1213
+ }
1214
+ out.invalid = invalid;
1215
+ return out;
1216
+ };
1217
+ /** `Range: 5-14` (with `Range-Unit: items`, or none). */
1218
+ const parseRange = (range, unit) => {
1219
+ if (!range) return void 0;
1220
+ if (unit && unit.toLowerCase() !== "items") return void 0;
1221
+ const m = /^(?:items=)?(\d+)-(\d*)$/.exec(range.trim());
1222
+ if (!m) return void 0;
1223
+ const start = Number(m[1]);
1224
+ if (m[2] === "") return { offset: start };
1225
+ const end = Number(m[2]);
1226
+ if (end < start) throw new PgRestError(416, "PGRST103", "Requested range not satisfiable", `The lower boundary must be lower than or equal to the upper boundary in the Range header.`, null);
1227
+ return {
1228
+ offset: start,
1229
+ limit: end - start + 1
1230
+ };
1231
+ };
1232
+ const parseAcceptEntry = (entry) => {
1233
+ const [type = "", ...params] = entry.split(";").map((s) => s.trim());
1234
+ let q = 1;
1235
+ let stripNulls = false;
1236
+ let forType;
1237
+ let options = [];
1238
+ for (const p of params) {
1239
+ const eq = p.indexOf("=");
1240
+ const k = (eq < 0 ? p : p.slice(0, eq)).trim();
1241
+ const v = (eq < 0 ? "" : p.slice(eq + 1)).trim().replace(/^"|"$/g, "");
1242
+ if (k === "q") q = Number(v);
1243
+ else if (k === "nulls" && v === "stripped") stripNulls = true;
1244
+ else if (k === "for") forType = v;
1245
+ else if (k === "options") options = v.split("|").filter((o) => PLAN_OPTIONS.has(o));
1246
+ }
1247
+ return {
1248
+ type: type.toLowerCase(),
1249
+ q: Number.isNaN(q) ? 1 : q,
1250
+ stripNulls,
1251
+ forType,
1252
+ options
1253
+ };
1254
+ };
1255
+ const PLAN_OPTIONS = new Set([
1256
+ "analyze",
1257
+ "verbose",
1258
+ "settings",
1259
+ "buffers",
1260
+ "wal"
1261
+ ]);
1262
+ const describeEntry = (e) => {
1263
+ if (!e.type.startsWith("application/vnd.pgrst.plan")) return e.type;
1264
+ const fmt = e.type.endsWith("+json") ? "+json" : "+text";
1265
+ const opts = e.options.length ? `; options=${e.options.join("|")}` : "";
1266
+ return `application/vnd.pgrst.plan${fmt}; for="${e.forType ?? "application/json"}"${opts}`;
1267
+ };
1268
+ /** Pick an output format from the `Accept` header. */
1269
+ const parseAccept = (accept, isRoot, planEnabled = false, choices = []) => {
1270
+ const entries = (accept ?? "*/*").split(",").map((e) => e.trim()).filter(Boolean).map(parseAcceptEntry).sort((a, b) => b.q - a.q);
1271
+ if (entries.length === 0) entries.push({
1272
+ type: "*/*",
1273
+ q: 1,
1274
+ stripNulls: false,
1275
+ forType: void 0,
1276
+ options: []
1277
+ });
1278
+ for (const { type, stripNulls, forType, options } of entries) {
1279
+ if (type === "application/vnd.pgrst.plan" || type === "application/vnd.pgrst.plan+json" || type === "application/vnd.pgrst.plan+text") {
1280
+ if (!planEnabled || isRoot) continue;
1281
+ const inner = parseAccept(forType ?? "application/json", false, false);
1282
+ return {
1283
+ kind: "plan",
1284
+ format: type.endsWith("+json") ? "json" : "text",
1285
+ forType: forType ?? "application/json",
1286
+ options,
1287
+ inner
1288
+ };
1289
+ }
1290
+ const custom = choices.find((c) => c.mediaType === type);
1291
+ if (custom && !isRoot) return {
1292
+ kind: "handler",
1293
+ choice: custom
1294
+ };
1295
+ switch (type) {
1296
+ case "*/*":
1297
+ case "application/*":
1298
+ case "application/json":
1299
+ case "application/openapi+json":
1300
+ if (isRoot) return { kind: "openapi" };
1301
+ if (type !== "application/openapi+json") return {
1302
+ kind: "json",
1303
+ stripNulls
1304
+ };
1305
+ break;
1306
+ case "application/vnd.pgrst.array+json": return {
1307
+ kind: "json",
1308
+ stripNulls
1309
+ };
1310
+ case "application/vnd.pgrst.object+json":
1311
+ case "application/vnd.pgrst.object": return {
1312
+ kind: "object",
1313
+ stripNulls
1314
+ };
1315
+ case "text/csv": return { kind: "csv" };
1316
+ case "application/geo+json":
1317
+ if (!isRoot) return { kind: "geojson" };
1318
+ break;
1319
+ }
1320
+ }
1321
+ throw new PgRestError(406, "PGRST107", "None of these media types are available: " + entries.map(describeEntry).join(", "));
1322
+ };
1323
+ /** Decode the request body into a JSON string (array of objects, or an object). */
1324
+ const parseBody = async (request, method) => {
1325
+ if (method !== "POST" && method !== "PATCH" && method !== "PUT") return void 0;
1326
+ const ct = (request.headers.get("content-type") ?? "application/json").split(";")[0].trim().toLowerCase();
1327
+ const text = await request.text();
1328
+ if (ct === "application/json" || ct === "application/vnd.pgrst.array+json") {
1329
+ if (text.trim() === "") return void 0;
1330
+ try {
1331
+ return {
1332
+ kind: "json",
1333
+ value: JSON.parse(text)
1334
+ };
1335
+ } catch {
1336
+ throw new PgRestError(400, "PGRST102", "Empty or invalid json");
1337
+ }
1338
+ }
1339
+ if (ct === "application/x-www-form-urlencoded") {
1340
+ const obj = {};
1341
+ for (const [k, v] of new URLSearchParams(text)) obj[k] = v;
1342
+ return {
1343
+ kind: "urlencoded",
1344
+ value: obj
1345
+ };
1346
+ }
1347
+ if (ct === "text/csv") return {
1348
+ kind: "csv",
1349
+ value: parseCsv(text)
1350
+ };
1351
+ throw new PgRestError(415, "PGRST107", `Content-Type not acceptable: ${ct}`);
1352
+ };
1353
+ /** Minimal RFC 4180 CSV → array of objects (first row is the header). */
1354
+ const parseCsv = (text) => {
1355
+ const rows = [];
1356
+ let row = [];
1357
+ let field$1 = "";
1358
+ let quoted = false;
1359
+ let sawAny = false;
1360
+ for (let i = 0; i < text.length; i++) {
1361
+ const ch = text[i];
1362
+ sawAny = true;
1363
+ if (quoted) if (ch === "\"") if (text[i + 1] === "\"") {
1364
+ field$1 += "\"";
1365
+ i++;
1366
+ } else quoted = false;
1367
+ else field$1 += ch;
1368
+ else if (ch === "\"") quoted = true;
1369
+ else if (ch === ",") {
1370
+ row.push(field$1);
1371
+ field$1 = "";
1372
+ } else if (ch === "\n" || ch === "\r") {
1373
+ if (ch === "\r" && text[i + 1] === "\n") i++;
1374
+ row.push(field$1);
1375
+ rows.push(row);
1376
+ row = [];
1377
+ field$1 = "";
1378
+ sawAny = false;
1379
+ } else field$1 += ch;
1380
+ }
1381
+ if (sawAny || field$1 !== "" || row.length > 0) {
1382
+ row.push(field$1);
1383
+ rows.push(row);
1384
+ }
1385
+ const [header, ...data] = rows;
1386
+ if (!header) return [];
1387
+ if (header.some((h) => h === "")) throw parseError("Invalid CSV", "the header row has an empty column name");
1388
+ 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
+ };
1390
+
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
+ //#endregion
1817
+ //#region src/media.ts
1818
+ const choiceFor = (mediaType, bytea, extra) => ({
1819
+ mediaType,
1820
+ contentType: mediaType === "*/*" || bytea ? mediaType === "*/*" ? "application/octet-stream" : mediaType : `${mediaType}; charset=utf-8`,
1821
+ bytea,
1822
+ ...extra
1823
+ });
1824
+ /** Aggregates in the exposed schemas that render rows of `relation` (or of any type) as a custom media type. */
1825
+ const aggregateChoices = (cache, schema, relation, allowTyped) => cache.mediaHandlers.filter((h) => h.schema === schema).filter((h) => h.argRelation ? allowTyped && h.argRelation.schema === relation.schema && h.argRelation.name === relation.name : true).map((h) => choiceFor(h.mediaType, h.bytea, {
1826
+ aggregate: quoteQualified(h.schema, h.mediaType),
1827
+ ...h.argRelation ? { rowType: quoteQualified(h.argRelation.schema, h.argRelation.name) } : {}
1828
+ }));
1829
+ /**
1830
+ * The custom media types a request can be answered with. Handlers that take a specific table apply only when
1831
+ * the request returns whole rows of it (no `select`); `anyelement` handlers apply to any rows.
1832
+ */
1833
+ const mediaChoices = (cache, schema, target, hasSelect) => {
1834
+ if (cache.mediaHandlers.length === 0 && ![...cache.functions.values()].some((fs) => fs.some((f) => f.returnMediaType))) return [];
1835
+ if (target.type === "table") {
1836
+ const rel = cache.relations.get(relKey(schema, target.name));
1837
+ return rel ? aggregateChoices(cache, schema, rel, !hasSelect) : [];
1838
+ }
1839
+ const fns = cache.functions.get(relKey(schema, target.name)) ?? [];
1840
+ const out = [];
1841
+ for (const f of fns) if (f.returnMediaType) out.push(choiceFor(f.returnMediaType.name.toLowerCase(), f.returnMediaType.bytea, {}));
1842
+ else if (f.returnRelation && f.returnsSet) out.push(...aggregateChoices(cache, schema, f.returnRelation, !hasSelect));
1843
+ if (fns.some((f) => !f.returnsSet && !f.returnsComposite && !f.returnsVoid)) {
1844
+ const any = cache.mediaHandlers.find((h) => h.schema === schema && h.mediaType === "*/*" && !h.argRelation);
1845
+ if (any) out.push(choiceFor("*/*", any.bytea, {}));
1846
+ }
1847
+ return out;
1848
+ };
1849
+
1850
+ //#endregion
1851
+ //#region src/openapi.ts
1852
+ /** Map a Postgres `format_type` string to an OpenAPI 2.0 schema fragment. */
1853
+ const pgTypeToOpenApi = (pgType) => {
1854
+ const t = pgType.trim();
1855
+ if (t.endsWith("[]")) return {
1856
+ type: "array",
1857
+ items: pgTypeToOpenApi(t.slice(0, -2))
1858
+ };
1859
+ const base = t.replace(/\(.*\)$/, "");
1860
+ switch (base) {
1861
+ case "smallint":
1862
+ case "integer": return {
1863
+ type: "integer",
1864
+ format: base
1865
+ };
1866
+ case "bigint": return {
1867
+ type: "integer",
1868
+ format: "bigint"
1869
+ };
1870
+ case "numeric":
1871
+ case "decimal": return {
1872
+ type: "number",
1873
+ format: "numeric"
1874
+ };
1875
+ case "real": return {
1876
+ type: "number",
1877
+ format: "real"
1878
+ };
1879
+ case "double precision": return {
1880
+ type: "number",
1881
+ format: "double precision"
1882
+ };
1883
+ case "boolean": return {
1884
+ type: "boolean",
1885
+ format: "boolean"
1886
+ };
1887
+ case "json":
1888
+ case "jsonb": return { format: base };
1889
+ case "uuid":
1890
+ case "text":
1891
+ case "date":
1892
+ case "bytea":
1893
+ case "money":
1894
+ case "interval": return {
1895
+ type: "string",
1896
+ format: base
1897
+ };
1898
+ case "character varying":
1899
+ case "character": return {
1900
+ type: "string",
1901
+ format: t
1902
+ };
1903
+ case "timestamp with time zone":
1904
+ case "timestamp without time zone":
1905
+ case "time with time zone":
1906
+ case "time without time zone": return {
1907
+ type: "string",
1908
+ format: base
1909
+ };
1910
+ default: return {
1911
+ type: "string",
1912
+ format: t
1913
+ };
1914
+ }
1915
+ };
1916
+ const columnSchema = (rel, c) => {
1917
+ const s = pgTypeToOpenApi(c.type);
1918
+ const notes = [];
1919
+ if (c.comment) notes.push(c.comment);
1920
+ if (rel.pk.includes(c.name)) notes.push("Note:\nThis is a Primary Key.<pk/>");
1921
+ if (notes.length) s["description"] = notes.join("\n\n");
1922
+ if (c.hasDefault && c.defaultExpr && !c.defaultExpr.includes("nextval(")) s["default"] = c.defaultExpr;
1923
+ return s;
1924
+ };
1925
+ const FILTER_NOTE = "Filter using PostgREST operators, e.g. `eq.1`, `gt.5`, `in.(a,b)`, `is.null`";
1926
+ /** Build the Swagger 2.0 document PostgREST serves at `/`. */
1927
+ const buildOpenApi = (cache, schema, opts) => {
1928
+ let host = "localhost";
1929
+ let scheme = "http";
1930
+ try {
1931
+ const u = new URL(opts.serverUrl);
1932
+ host = u.host;
1933
+ scheme = u.protocol.replace(":", "");
1934
+ } catch {}
1935
+ const relations = [...cache.relations.values()].filter((r) => r.schema === schema).sort((a, b) => a.name.localeCompare(b.name));
1936
+ const functions = [...cache.functions.values()].flat().filter((f) => f.schema === schema).sort((a, b) => a.name.localeCompare(b.name));
1937
+ const parameters = {
1938
+ preferReturn: {
1939
+ name: "Prefer",
1940
+ description: "Preference",
1941
+ required: false,
1942
+ in: "header",
1943
+ type: "string",
1944
+ enum: [
1945
+ "return=representation",
1946
+ "return=minimal",
1947
+ "return=headers-only"
1948
+ ]
1949
+ },
1950
+ preferCount: {
1951
+ name: "Prefer",
1952
+ description: "Preference",
1953
+ required: false,
1954
+ in: "header",
1955
+ type: "string",
1956
+ enum: ["count=none"]
1957
+ },
1958
+ preferPost: {
1959
+ name: "Prefer",
1960
+ description: "Preference",
1961
+ required: false,
1962
+ in: "header",
1963
+ type: "string",
1964
+ enum: [
1965
+ "return=representation",
1966
+ "return=minimal",
1967
+ "return=headers-only",
1968
+ "resolution=ignore-duplicates",
1969
+ "resolution=merge-duplicates"
1970
+ ]
1971
+ },
1972
+ select: {
1973
+ name: "select",
1974
+ description: "Filtering Columns",
1975
+ required: false,
1976
+ in: "query",
1977
+ type: "string"
1978
+ },
1979
+ on_conflict: {
1980
+ name: "on_conflict",
1981
+ description: "On Conflict",
1982
+ required: false,
1983
+ in: "query",
1984
+ type: "string"
1985
+ },
1986
+ order: {
1987
+ name: "order",
1988
+ description: "Ordering",
1989
+ required: false,
1990
+ in: "query",
1991
+ type: "string"
1992
+ },
1993
+ range: {
1994
+ name: "Range",
1995
+ description: "Limiting and Pagination",
1996
+ required: false,
1997
+ in: "header",
1998
+ type: "string"
1999
+ },
2000
+ rangeUnit: {
2001
+ name: "Range-Unit",
2002
+ description: "Limiting and Pagination",
2003
+ required: false,
2004
+ default: "items",
2005
+ in: "header",
2006
+ type: "string"
2007
+ },
2008
+ offset: {
2009
+ name: "offset",
2010
+ description: "Limiting and Pagination",
2011
+ required: false,
2012
+ in: "query",
2013
+ type: "string"
2014
+ },
2015
+ limit: {
2016
+ name: "limit",
2017
+ description: "Limiting and Pagination",
2018
+ required: false,
2019
+ in: "query",
2020
+ type: "string"
2021
+ }
2022
+ };
2023
+ const paths = { "/": { get: {
2024
+ tags: ["Introspection"],
2025
+ summary: "OpenAPI description (this document)",
2026
+ produces: ["application/openapi+json", "application/json"],
2027
+ responses: { "200": { description: "OK" } }
2028
+ } } };
2029
+ const definitions = {};
2030
+ for (const rel of relations) {
2031
+ const props = {};
2032
+ const required = [];
2033
+ for (const c of rel.columns) {
2034
+ props[c.name] = columnSchema(rel, c);
2035
+ if (!c.nullable && !c.hasDefault) required.push(c.name);
2036
+ }
2037
+ definitions[rel.name] = {
2038
+ type: "object",
2039
+ ...required.length ? { required } : {},
2040
+ properties: props,
2041
+ ...rel.comment ? { description: rel.comment } : {}
2042
+ };
2043
+ const filterRefs = [];
2044
+ for (const c of rel.columns) {
2045
+ const key = `rowFilter.${rel.name}.${c.name}`;
2046
+ parameters[key] = {
2047
+ name: c.name,
2048
+ required: false,
2049
+ in: "query",
2050
+ type: "string",
2051
+ description: FILTER_NOTE
2052
+ };
2053
+ filterRefs.push({ $ref: `#/parameters/${key}` });
2054
+ }
2055
+ const bodyKey = `body.${rel.name}`;
2056
+ parameters[bodyKey] = {
2057
+ name: rel.name,
2058
+ description: rel.name,
2059
+ required: false,
2060
+ in: "body",
2061
+ schema: { $ref: `#/definitions/${rel.name}` }
2062
+ };
2063
+ const ref = `#/definitions/${rel.name}`;
2064
+ const op = { get: {
2065
+ tags: [rel.name],
2066
+ parameters: [
2067
+ ...filterRefs,
2068
+ { $ref: "#/parameters/select" },
2069
+ { $ref: "#/parameters/order" },
2070
+ { $ref: "#/parameters/range" },
2071
+ { $ref: "#/parameters/rangeUnit" },
2072
+ { $ref: "#/parameters/offset" },
2073
+ { $ref: "#/parameters/limit" },
2074
+ { $ref: "#/parameters/preferCount" }
2075
+ ],
2076
+ responses: {
2077
+ "200": {
2078
+ description: "OK",
2079
+ schema: {
2080
+ type: "array",
2081
+ items: { $ref: ref }
2082
+ }
2083
+ },
2084
+ "206": { description: "Partial Content" }
2085
+ },
2086
+ ...rel.comment ? { summary: rel.comment } : {}
2087
+ } };
2088
+ if (rel.insertable) op["post"] = {
2089
+ tags: [rel.name],
2090
+ parameters: [
2091
+ { $ref: `#/parameters/${bodyKey}` },
2092
+ { $ref: "#/parameters/select" },
2093
+ { $ref: "#/parameters/preferPost" }
2094
+ ],
2095
+ responses: { "201": { description: "Created" } }
2096
+ };
2097
+ if (rel.deletable) op["delete"] = {
2098
+ tags: [rel.name],
2099
+ parameters: [...filterRefs, { $ref: "#/parameters/preferReturn" }],
2100
+ responses: { "204": { description: "No Content" } }
2101
+ };
2102
+ if (rel.updatable) op["patch"] = {
2103
+ tags: [rel.name],
2104
+ parameters: [
2105
+ ...filterRefs,
2106
+ { $ref: `#/parameters/${bodyKey}` },
2107
+ { $ref: "#/parameters/preferReturn" }
2108
+ ],
2109
+ responses: { "204": { description: "No Content" } }
2110
+ };
2111
+ paths[`/${rel.name}`] = op;
2112
+ }
2113
+ const seen = /* @__PURE__ */ new Set();
2114
+ for (const fn of functions) {
2115
+ if (seen.has(fn.name)) continue;
2116
+ seen.add(fn.name);
2117
+ paths[`/rpc/${fn.name}`] = rpcPath(fn);
2118
+ }
2119
+ return {
2120
+ swagger: "2.0",
2121
+ info: {
2122
+ title: `standard ${schema} schema`,
2123
+ description: "",
2124
+ version: "16"
2125
+ },
2126
+ host,
2127
+ basePath: opts.basePath || "/",
2128
+ schemes: [scheme],
2129
+ consumes: [
2130
+ "application/json",
2131
+ "application/vnd.pgrst.object+json;nulls=stripped",
2132
+ "application/vnd.pgrst.object+json",
2133
+ "text/csv"
2134
+ ],
2135
+ produces: [
2136
+ "application/json",
2137
+ "application/vnd.pgrst.object+json;nulls=stripped",
2138
+ "application/vnd.pgrst.object+json",
2139
+ "text/csv"
2140
+ ],
2141
+ paths,
2142
+ definitions,
2143
+ parameters,
2144
+ externalDocs: {
2145
+ description: "PostgREST Documentation",
2146
+ url: "https://postgrest.org/en/v16/references/api.html"
2147
+ }
2148
+ };
2149
+ };
2150
+ const rpcPath = (fn) => {
2151
+ const props = {};
2152
+ const required = [];
2153
+ for (const a of fn.args) {
2154
+ if (!a.name) continue;
2155
+ props[a.name] = pgTypeToOpenApi(a.type);
2156
+ if (!a.hasDefault) required.push(a.name);
2157
+ }
2158
+ const path = { post: {
2159
+ tags: [`(rpc) ${fn.name}`],
2160
+ produces: [
2161
+ "application/json",
2162
+ "application/vnd.pgrst.object+json;nulls=stripped",
2163
+ "application/vnd.pgrst.object+json"
2164
+ ],
2165
+ parameters: [{
2166
+ name: "args",
2167
+ required: true,
2168
+ in: "body",
2169
+ schema: {
2170
+ type: "object",
2171
+ properties: props,
2172
+ ...required.length ? { required } : {}
2173
+ }
2174
+ }],
2175
+ responses: { "200": { description: "OK" } },
2176
+ ...fn.comment ? { summary: fn.comment } : {}
2177
+ } };
2178
+ if (fn.volatility !== "volatile") path["get"] = {
2179
+ tags: [`(rpc) ${fn.name}`],
2180
+ produces: [
2181
+ "application/json",
2182
+ "application/vnd.pgrst.object+json;nulls=stripped",
2183
+ "application/vnd.pgrst.object+json"
2184
+ ],
2185
+ parameters: fn.args.filter((a) => a.name).map((a) => ({
2186
+ name: a.name,
2187
+ required: !a.hasDefault,
2188
+ in: "query",
2189
+ ...pgTypeToOpenApi(a.type)
2190
+ })),
2191
+ responses: { "200": { description: "OK" } }
2192
+ };
2193
+ return path;
2194
+ };
2195
+
2196
+ //#endregion
2197
+ //#region src/sql/filters.ts
2198
+ /** `"alias"."column"->'a'->>'b'` */
2199
+ const fieldSql = (alias, field$1) => {
2200
+ let sql = `${quoteIdent(alias)}.${quoteIdent(field$1.name)}`;
2201
+ for (const step of field$1.json) sql += ` ${step.arrow} ${step.isIndex ? String(Number(step.key)) : literal(step.key)}`;
2202
+ return sql;
2203
+ };
2204
+ const COMPARISON = {
2205
+ eq: "=",
2206
+ neq: "<>",
2207
+ gt: ">",
2208
+ gte: ">=",
2209
+ lt: "<",
2210
+ lte: "<=",
2211
+ like: "LIKE",
2212
+ ilike: "ILIKE",
2213
+ match: "~",
2214
+ imatch: "~*",
2215
+ cs: "@>",
2216
+ cd: "<@",
2217
+ ov: "&&",
2218
+ sl: "<<",
2219
+ sr: ">>",
2220
+ nxl: "&>",
2221
+ nxr: "&<",
2222
+ adj: "-|-"
2223
+ };
2224
+ const FTS_FUNCTIONS = {
2225
+ fts: "to_tsquery",
2226
+ plfts: "plainto_tsquery",
2227
+ phfts: "phraseto_tsquery",
2228
+ wfts: "websearch_to_tsquery"
2229
+ };
2230
+ const columnType = (rel, field$1) => field$1.json.length === 0 ? rel?.columns.find((c) => c.name === field$1.name)?.type : void 0;
2231
+ const patternValue = (op, v) => op === "like" || op === "ilike" ? v.replace(/\*/g, "%") : v;
2232
+ /** SQL boolean expression for one filter. */
2233
+ const filterSql = (f, alias, rel) => {
2234
+ const lhs = fieldSql(alias, f.field);
2235
+ let expr;
2236
+ switch (f.op) {
2237
+ case "in": {
2238
+ const list = f.value;
2239
+ expr = list.length === 0 ? "FALSE" : `${lhs} IN (${list.map(literal).join(", ")})`;
2240
+ break;
2241
+ }
2242
+ case "is":
2243
+ switch (f.value) {
2244
+ case "null":
2245
+ expr = `${lhs} IS NULL`;
2246
+ break;
2247
+ case "not_null":
2248
+ expr = `${lhs} IS NOT NULL`;
2249
+ break;
2250
+ case "true":
2251
+ expr = `${lhs} IS TRUE`;
2252
+ break;
2253
+ case "false":
2254
+ expr = `${lhs} IS FALSE`;
2255
+ break;
2256
+ default: expr = `${lhs} IS UNKNOWN`;
2257
+ }
2258
+ break;
2259
+ case "isdistinct":
2260
+ expr = `${lhs} IS DISTINCT FROM ${literal(f.value)}`;
2261
+ break;
2262
+ case "fts":
2263
+ case "plfts":
2264
+ case "phfts":
2265
+ case "wfts": {
2266
+ const fn = FTS_FUNCTIONS[f.op];
2267
+ const cfg = f.lang ? `${literal(f.lang)}, ` : "";
2268
+ expr = `${columnType(rel, f.field) === "tsvector" ? lhs : `to_tsvector(${cfg}${lhs})`} @@ ${fn}(${cfg}${literal(f.value)})`;
2269
+ break;
2270
+ }
2271
+ default: {
2272
+ const sqlOp = COMPARISON[f.op];
2273
+ const value = patternValue(f.op, f.value);
2274
+ expr = f.quantifier ? `${lhs} ${sqlOp} ${f.quantifier.toUpperCase()} (${literal(value)})` : `${lhs} ${sqlOp} ${literal(value)}`;
2275
+ }
2276
+ }
2277
+ return f.negate ? `NOT (${expr})` : expr;
2278
+ };
2279
+ const nodeSql = (node, alias, rel) => {
2280
+ if (node.kind === "filter") return filterSql(node, alias, rel);
2281
+ const joiner = node.op === "and" ? " AND " : " OR ";
2282
+ const wrapped = `(${node.children.map((c) => nodeSql(c, alias, rel)).join(joiner)})`;
2283
+ return node.negate ? `NOT ${wrapped}` : wrapped;
2284
+ };
2285
+ const whereSql = (nodes, alias, rel) => nodes.map((n) => nodeSql(n, alias, rel));
2286
+
2287
+ //#endregion
2288
+ //#region src/sql/read.ts
2289
+ const ROOT_ALIAS = "pgrst_root";
2290
+ var Aliases = class {
2291
+ n = 0;
2292
+ next(prefix) {
2293
+ return `${prefix}${++this.n}`;
2294
+ }
2295
+ };
2296
+ const fieldExpr = (item, alias) => {
2297
+ const base = fieldSql(alias, item.field);
2298
+ const expr = item.cast ? `${base}::${item.cast}` : base;
2299
+ return !item.cast && item.field.json.length === 0 && !item.alias ? expr : `${expr} AS ${quoteIdent(outputName(item))}`;
2300
+ };
2301
+ const aggregateExpr = (item, alias) => {
2302
+ const arg = item.field ? fieldSql(alias, item.field) : "*";
2303
+ let expr = `${item.fn}(${arg})`;
2304
+ if (item.cast) expr = `(${expr})::${item.cast}`;
2305
+ return `${expr} AS ${quoteIdent(outputName(item))}`;
2306
+ };
2307
+ /** Output column names produced by a list of select items (used for spreads). */
2308
+ const itemNames = (ctx, items, rel) => {
2309
+ const out = [];
2310
+ for (const it of items) if (it.kind === "star") out.push(...rel.columns.map((c) => c.name));
2311
+ else if (it.kind === "field" || it.kind === "aggregate") out.push(outputName(it));
2312
+ else if (it.items.length === 0) continue;
2313
+ else if (it.spread) out.push(...itemNames(ctx, it.items, resolveRelationship(ctx.cache, rel, it.target, it.hint).to));
2314
+ else out.push(it.alias ?? it.target);
2315
+ return out;
2316
+ };
2317
+ const orderClause = (ctx, terms, rel, alias, items) => {
2318
+ if (terms.length === 0) return "";
2319
+ const q = quoteIdent;
2320
+ return `ORDER BY ${terms.map((t) => {
2321
+ const tail = `${t.dir.toUpperCase()}${t.nulls ? ` NULLS ${t.nulls.toUpperCase()}` : ""}`;
2322
+ if (!t.relation) return `${fieldSql(alias, t.field)} ${tail}`;
2323
+ const embed = items.find((i) => i.kind === "embed" && (i.alias ?? i.target) === t.relation);
2324
+ if (!embed) throw new PgRestError(400, "PGRST108", `'${t.relation}' is not an embedded resource in this request`, null, `Verify that '${t.relation}' is included in the 'select' query parameter.`);
2325
+ const r = resolveRelationship(ctx.cache, rel, embed.target, embed.hint);
2326
+ if (r.kind !== "m2o" && r.kind !== "o2o") throw new PgRestError(400, "PGRST118", `A related order on '${t.relation}' is not possible`, `'${rel.name}' and '${embed.target}' do not form a many-to-one or one-to-one relationship`, null);
2327
+ const selected = embed.items.filter((i) => i.kind !== "embed");
2328
+ if (selected.length > 0 && !selected.some((i) => i.kind === "star" || i.kind === "field" && i.field.name === t.field.name)) throw new PgRestError(400, "42703", `column ${rel.name}_${embed.target}_1.${t.field.name} does not exist`, null, null);
2329
+ const o = ctx.aliases.next("pgrst_o");
2330
+ if (r.computed) return `(SELECT ${fieldSql(o, t.field)} FROM ${quoteQualified(r.computed.schema, r.computed.name)}(${q(alias)}) AS ${q(o)} LIMIT 1) ${tail}`;
2331
+ const cond = r.toCols.map((c, i) => `${q(o)}.${q(c)} = ${q(alias)}.${q(r.fromCols[i])}`).join(" AND ");
2332
+ return `(SELECT ${fieldSql(o, t.field)} FROM ${quoteQualified(r.to.schema, r.to.name)} AS ${q(o)} WHERE ${cond} LIMIT 1) ${tail}`;
2333
+ }).join(", ")}`;
2334
+ };
2335
+ const buildItems = (ctx, items, rel, alias, path) => {
2336
+ const cols = [];
2337
+ const joins = [];
2338
+ const groupBy = [];
2339
+ const aggregated = items.some((i) => i.kind === "aggregate");
2340
+ for (const item of items) switch (item.kind) {
2341
+ case "star":
2342
+ if (aggregated) for (const c of rel.columns) {
2343
+ const e = `${quoteIdent(alias)}.${quoteIdent(c.name)}`;
2344
+ cols.push(e);
2345
+ groupBy.push(e);
2346
+ }
2347
+ else cols.push(`${quoteIdent(alias)}.*`);
2348
+ break;
2349
+ case "field":
2350
+ cols.push(fieldExpr(item, alias));
2351
+ if (aggregated) groupBy.push(fieldSql(alias, item.field));
2352
+ break;
2353
+ case "aggregate":
2354
+ cols.push(aggregateExpr(item, alias));
2355
+ break;
2356
+ case "embed": {
2357
+ const e = buildEmbed(ctx, item, rel, alias, path);
2358
+ cols.push(...e.cols);
2359
+ joins.push(e.join);
2360
+ if (aggregated) groupBy.push(...e.group);
2361
+ break;
2362
+ }
2363
+ }
2364
+ return {
2365
+ cols,
2366
+ joins,
2367
+ groupBy,
2368
+ aggregated
2369
+ };
2370
+ };
2371
+ const presenceConditions = (params, joins) => (params?.presence ?? []).map((p) => {
2372
+ const j = joins.find((x) => x.name === p.embed);
2373
+ if (!j) throw new PgRestError(400, "PGRST108", `'${p.embed}' is not an embedded resource in this request`, null, `Verify that '${p.embed}' is included in the 'select' query parameter.`);
2374
+ return `${quoteIdent(j.alias)}.j IS ${p.present ? "NOT " : ""}NULL`;
2375
+ });
2376
+ const paging = (limit, offset) => {
2377
+ let s = "";
2378
+ if (limit !== void 0) s += ` LIMIT ${Math.trunc(limit)}`;
2379
+ if (offset !== void 0 && offset > 0) s += ` OFFSET ${Math.trunc(offset)}`;
2380
+ return s;
2381
+ };
2382
+ const buildEmbed = (ctx, item, parentRel, parentAlias, path) => {
2383
+ const relationship = resolveRelationship(ctx.cache, parentRel, item.target, item.hint);
2384
+ const name = item.alias ?? item.target;
2385
+ const childPath = [...path, name];
2386
+ const params = ctx.query.embeds.get(childPath.join("."));
2387
+ const toOne = relationship.kind === "m2o" || relationship.kind === "o2o";
2388
+ const child = relationship.to;
2389
+ const cAlias = ctx.aliases.next("pgrst_c");
2390
+ const eAlias = ctx.aliases.next("pgrst_j");
2391
+ const inner = buildItems(ctx, item.items, child, cAlias, childPath);
2392
+ const hasCols = inner.cols.length > 0;
2393
+ const q = quoteIdent;
2394
+ const conds = [];
2395
+ let from;
2396
+ if (relationship.computed) {
2397
+ const fn = relationship.computed;
2398
+ from = `${quoteQualified(fn.schema, fn.name)}(${q(parentAlias)}) AS ${q(cAlias)}`;
2399
+ conds.push("TRUE");
2400
+ } else if (relationship.junction) {
2401
+ const j = relationship.junction;
2402
+ const jAlias = ctx.aliases.next("pgrst_m");
2403
+ const onTo = j.fkTo.cols.map((c, i) => `${q(jAlias)}.${q(c)} = ${q(cAlias)}.${q(j.fkTo.fcols[i])}`);
2404
+ from = `${quoteQualified(child.schema, child.name)} AS ${q(cAlias)} JOIN ${quoteQualified(j.relation.schema, j.relation.name)} AS ${q(jAlias)} ON ${onTo.join(" AND ")}`;
2405
+ conds.push(...j.fkFrom.cols.map((c, i) => `${q(jAlias)}.${q(c)} = ${q(parentAlias)}.${q(j.fkFrom.fcols[i])}`));
2406
+ } else {
2407
+ from = `${quoteQualified(child.schema, child.name)} AS ${q(cAlias)}`;
2408
+ conds.push(...relationship.toCols.map((c, i) => `${q(cAlias)}.${q(c)} = ${q(parentAlias)}.${q(relationship.fromCols[i])}`));
2409
+ }
2410
+ if (inner.joins.length) from += ` ${inner.joins.map((j) => j.sql).join(" ")}`;
2411
+ conds.push(...params ? whereSql(params.filters, cAlias, child) : []);
2412
+ conds.push(...presenceConditions(params, inner.joins));
2413
+ const order$1 = params ? orderClause(ctx, params.order, child, cAlias, item.items) : "";
2414
+ const page = paging(params?.limit, params?.offset);
2415
+ const sel = hasCols ? inner.cols.join(", ") : "1";
2416
+ const groupSql = inner.aggregated && inner.groupBy.length ? ` GROUP BY ${inner.groupBy.join(", ")}` : "";
2417
+ const body = `SELECT ${sel} FROM ${from} WHERE ${conds.join(" AND ")}${groupSql}${order$1 ? ` ${order$1}` : ""}${page}`;
2418
+ const J = "jsonb";
2419
+ const joinKw = item.join === "inner" ? "JOIN LATERAL" : "LEFT JOIN LATERAL";
2420
+ if (item.spread) {
2421
+ const names = itemNames(ctx, item.items, child);
2422
+ if (toOne) {
2423
+ const lateral$2 = `${joinKw} (${body}${page ? "" : " LIMIT 1"}) AS ${q(eAlias)} ON TRUE`;
2424
+ const refs$1 = names.map((n) => `${q(eAlias)}.${q(n)}`);
2425
+ return {
2426
+ cols: names.map((n, i) => `${refs$1[i]} AS ${q(n)}`),
2427
+ join: {
2428
+ sql: lateral$2,
2429
+ alias: eAlias,
2430
+ name,
2431
+ inner: item.join === "inner"
2432
+ },
2433
+ group: refs$1
2434
+ };
2435
+ }
2436
+ if (inner.aggregated) throw new PgRestError(400, "PGRST127", "Feature not implemented", "Aggregates are not implemented for one-to-many or many-to-many spreads.", null);
2437
+ const agg = names.map((n) => `coalesce(${J}_agg(_r.${q(n)}), '[]'::${J}) AS ${q(n)}`).join(", ");
2438
+ const having = item.join === "inner" ? " HAVING count(*) > 0" : "";
2439
+ const lateral$1 = `${joinKw} (SELECT ${agg || "1"} FROM (${body}) AS _r${having}) AS ${q(eAlias)} ON TRUE`;
2440
+ const refs = names.map((n) => `${q(eAlias)}.${q(n)}`);
2441
+ return {
2442
+ cols: names.map((n, i) => `${refs[i]} AS ${q(n)}`),
2443
+ join: {
2444
+ sql: lateral$1,
2445
+ alias: eAlias,
2446
+ name,
2447
+ inner: item.join === "inner"
2448
+ },
2449
+ group: refs
2450
+ };
2451
+ }
2452
+ if (toOne) {
2453
+ const lateral$1 = `${joinKw} (SELECT to_${J}(_r) AS j FROM (${body}) AS _r LIMIT 1) AS ${q(eAlias)} ON TRUE`;
2454
+ return {
2455
+ cols: hasCols ? [`${q(eAlias)}.j AS ${q(name)}`] : [],
2456
+ join: {
2457
+ sql: lateral$1,
2458
+ alias: eAlias,
2459
+ name,
2460
+ inner: item.join === "inner"
2461
+ },
2462
+ group: hasCols ? [`${q(eAlias)}.j`] : []
2463
+ };
2464
+ }
2465
+ const lateral = `${joinKw} (SELECT ${J}_agg(_r) AS j FROM (${body}) AS _r${item.join === "inner" ? " HAVING count(*) > 0" : ""}) AS ${q(eAlias)} ON TRUE`;
2466
+ return {
2467
+ cols: hasCols ? [`coalesce(${q(eAlias)}.j, '[]'::${J}) AS ${q(name)}`] : [],
2468
+ join: {
2469
+ sql: lateral,
2470
+ alias: eAlias,
2471
+ name,
2472
+ inner: item.join === "inner"
2473
+ },
2474
+ group: hasCols ? [`${q(eAlias)}.j`] : []
2475
+ };
2476
+ };
2477
+ /** Build the SELECT (and matching COUNT) for a read request. */
2478
+ const buildRead = (opts) => {
2479
+ const ctx = {
2480
+ cache: opts.cache,
2481
+ query: opts.query,
2482
+ aliases: new Aliases()
2483
+ };
2484
+ const built = buildItems(ctx, opts.query.select, opts.rel, ROOT_ALIAS, []);
2485
+ const where = [...whereSql(opts.query.root.filters, ROOT_ALIAS, opts.rel), ...presenceConditions(opts.query.root, built.joins)];
2486
+ const whereClause = where.length ? ` WHERE ${where.join(" AND ")}` : "";
2487
+ const fromClause = `${opts.from} AS ${quoteIdent(ROOT_ALIAS)}`;
2488
+ const joinSql = built.joins.map((j) => j.sql).join(" ");
2489
+ const groupClause = built.aggregated && built.groupBy.length ? ` GROUP BY ${built.groupBy.join(", ")}` : "";
2490
+ const order$1 = orderClause(ctx, opts.query.root.order, opts.rel, ROOT_ALIAS, opts.query.select);
2491
+ const unpaged = `SELECT ${built.cols.length ? built.cols.join(", ") : "1"} FROM ${fromClause}${joinSql ? ` ${joinSql}` : ""}${whereClause}${groupClause}`;
2492
+ const rows = `${unpaged}${order$1 ? ` ${order$1}` : ""}${paging(opts.limit, opts.offset)}`;
2493
+ let count;
2494
+ if (built.aggregated) count = `SELECT count(*) FROM (${unpaged}) AS _c`;
2495
+ else if (built.joins.some((j) => j.inner) || (opts.query.root.presence.length ?? 0) > 0) count = `SELECT count(*) FROM ${fromClause} ${joinSql}${whereClause}`;
2496
+ else count = `SELECT count(*) FROM ${fromClause}${whereClause}`;
2497
+ return {
2498
+ rows,
2499
+ count,
2500
+ unpaged
2501
+ };
2502
+ };
2503
+
2504
+ //#endregion
2505
+ //#region src/sql/rpc.ts
2506
+ const SINGLE_BODY_TYPES = new Set([
2507
+ "json",
2508
+ "jsonb",
2509
+ "text",
2510
+ "bytea"
2511
+ ]);
2512
+ const isSingleUnnamedParam = (fn) => fn.args.length === 1 && fn.args[0].name === "" && SINGLE_BODY_TYPES.has(fn.args[0].type);
2513
+ /** PostgREST's spelling used in PGRST203: `schema.fn(a => integer, b => text)`. */
2514
+ const arrowSignature = (fn) => `${fn.schema}.${fn.name}(${fn.args.map((a) => `${a.name} => ${a.type}`).join(", ")})`;
2515
+ /** Similarity of two names, 1 for equal; PostgREST suggests the closest name above ~0.75. */
2516
+ const similarity = (a, b) => 1 - levenshtein(a.toLowerCase(), b.toLowerCase()) / Math.max(a.length, b.length, 1);
2517
+ const SUGGEST_AT = .75;
2518
+ /** The "Perhaps you meant ..." hint of PGRST202. */
2519
+ const functionHint = (cache, schema, name, candidates, provided) => {
2520
+ if (candidates.length > 0) {
2521
+ let best$1;
2522
+ for (const fn of candidates) {
2523
+ let score = 0;
2524
+ for (const p of provided) if (fn.args.some((a) => a.name === p)) score += 2;
2525
+ else if (fn.args.some((a) => similarity(a.name, p) >= SUGGEST_AT)) score += 1;
2526
+ if (score > 0 && (!best$1 || score > best$1.score)) best$1 = {
2527
+ fn,
2528
+ score
2529
+ };
2530
+ }
2531
+ return best$1 ? `Perhaps you meant to call the function ${schema}.${name}(${best$1.fn.args.map((a) => a.name).sort().join(", ")})` : null;
2532
+ }
2533
+ let best;
2534
+ for (const fns of cache.functions.values()) {
2535
+ const fn = fns[0];
2536
+ if (!fn || fn.schema !== schema) continue;
2537
+ const score = similarity(name, fn.name);
2538
+ if (score >= SUGGEST_AT && (!best || score > best.score)) best = {
2539
+ name: fn.name,
2540
+ score
2541
+ };
2542
+ }
2543
+ return best ? `Perhaps you meant to call the function ${schema}.${best.name}` : null;
2544
+ };
2545
+ /**
2546
+ * Pick the function to call from the argument names the client supplied.
2547
+ * `rawBody` is true when a request body that is not a JSON object was given;
2548
+ * `post` tells whether the function was invoked with POST (which also allows a single unnamed parameter).
2549
+ */
2550
+ const resolveFunction = (cache, schema, name, provided, hasBody, post = true) => {
2551
+ const candidates = cache.functions.get(relKey(schema, name)) ?? [];
2552
+ const names = [...provided].sort();
2553
+ const params = names.length === 0 ? "without parameters" : `with parameter${names.length === 1 ? "" : "s"} ${names.join(", ")}`;
2554
+ const notFound = () => new PgRestError(404, "PGRST202", names.length === 0 ? `Could not find the function ${schema}.${name} without parameters in the schema cache` : `Could not find the function ${schema}.${name}(${names.join(", ")}) in the schema cache`, `Searched for the function ${schema}.${name} ${params}${post ? " or with a single unnamed json/jsonb parameter" : ""}, but no matches were found in the schema cache.`, functionHint(cache, schema, name, candidates, provided));
2555
+ if (candidates.length === 0) throw notFound();
2556
+ const named = candidates.filter((fn) => {
2557
+ const argNames = new Set(fn.args.map((a) => a.name));
2558
+ for (const p of provided) if (!argNames.has(p)) return false;
2559
+ return fn.args.every((a) => a.hasDefault || provided.has(a.name));
2560
+ });
2561
+ const single = hasBody ? candidates.filter(isSingleUnnamedParam) : [];
2562
+ const matches = provided.size === 0 && hasBody && single.length > 0 ? single : [...named, ...single.filter((s) => !named.includes(s))];
2563
+ if (matches.length === 0) throw notFound();
2564
+ if (matches.length === 1) return matches[0];
2565
+ const exact = matches.filter((fn) => fn.args.length === provided.size);
2566
+ if (exact.length === 1) return exact[0];
2567
+ throw new PgRestError(300, "PGRST203", `Could not choose the best candidate function between: ${matches.map(arrowSignature).sort().join(", ")}`, null, "Try renaming the parameters or the function itself in the database so function overloading can be resolved");
2568
+ };
2569
+ const stripQuotes = (s) => s.replace(/^"|"$/g, "");
2570
+ /** Find the table a composite return type refers to, if it is an exposed relation. */
2571
+ const relationOfReturnType = (cache, fn) => {
2572
+ if (!fn.returnsComposite) return void 0;
2573
+ const t = fn.returnType.replace(/\[\]$/, "");
2574
+ const dot = t.indexOf(".");
2575
+ if (dot > 0) return cache.relations.get(relKey(stripQuotes(t.slice(0, dot)), stripQuotes(t.slice(dot + 1))));
2576
+ const bare = stripQuotes(t);
2577
+ const inOwn = cache.relations.get(relKey(fn.schema, bare));
2578
+ if (inOwn) return inOwn;
2579
+ for (const r of cache.relations.values()) if (r.name === bare) return r;
2580
+ };
2581
+ const syntheticRelation = (fn) => ({
2582
+ schema: fn.schema,
2583
+ name: fn.name,
2584
+ kind: "view",
2585
+ columns: [],
2586
+ pk: [],
2587
+ uniques: [],
2588
+ insertable: false,
2589
+ updatable: false,
2590
+ deletable: false,
2591
+ comment: null
2592
+ });
2593
+ /**
2594
+ * Build the SQL that calls `fn`. Arguments travel as one JSON text (`$1`) and
2595
+ * are converted by `json_to_record` using the declared types, so values are
2596
+ * never interpolated into SQL.
2597
+ */
2598
+ const buildRpc = (cache, fn, provided, rawBody) => {
2599
+ const fnRef = quoteQualified(fn.schema, fn.name);
2600
+ let args;
2601
+ let usesBody = false;
2602
+ let from = "";
2603
+ if (rawBody && isSingleUnnamedParam(fn)) {
2604
+ args = `($1::text)::${fn.args[0].type}`;
2605
+ usesBody = true;
2606
+ } else {
2607
+ const used = fn.args.filter((a) => provided.includes(a.name));
2608
+ args = used.map((a) => `${a.variadic ? "VARIADIC " : ""}${quoteIdent(a.name)} := _args.${quoteIdent(a.name)}`).join(", ");
2609
+ if (used.length > 0) {
2610
+ usesBody = true;
2611
+ from = `json_to_record(($1::text)::json) AS _args(${used.map((a) => `${quoteIdent(a.name)} ${a.type}`).join(", ")}) CROSS JOIN LATERAL `;
2612
+ }
2613
+ }
2614
+ const call = `${from}${fnRef}(${args}) AS _f`;
2615
+ const base = fn.returnsVoid ? `SELECT 1 AS pgrst_scalar FROM ${call}` : fn.returnsComposite ? `SELECT _f.* FROM ${call}` : `SELECT _f AS pgrst_scalar FROM ${call}`;
2616
+ const shape = fn.returnsVoid ? "void" : fn.returnsComposite ? fn.returnsSet ? "rows" : "object" : fn.returnsSet ? "scalar-set" : "scalar";
2617
+ const relation = relationOfReturnType(cache, fn) ?? syntheticRelation(fn);
2618
+ return {
2619
+ fn,
2620
+ shape,
2621
+ source: base,
2622
+ usesBody,
2623
+ relation
2624
+ };
2625
+ };
2626
+
2627
+ //#endregion
2628
+ //#region src/sql/statement.ts
2629
+ const CSV_BODY = "coalesce((SELECT string_agg(k, ',') FROM (SELECT json_object_keys(r)::text AS k FROM (SELECT row_to_json(hh) AS r FROM pgrst_body AS hh LIMIT 1) s) a), '') || E'\\n' || coalesce(string_agg(substring(_t::text, 2, length(_t::text) - 2), E'\\n'), '')";
2630
+ const handlerBody = (h) => {
2631
+ if (h.aggregate) {
2632
+ const call = `${h.aggregate}(${h.rowType ? `ROW(_t.*)::${h.rowType}` : "_t"})`;
2633
+ return h.bytea ? `coalesce(encode(${call}::bytea, 'hex'), '')` : `coalesce(${call}::text, '')`;
2634
+ }
2635
+ return h.bytea ? `coalesce(encode((array_agg(_t.pgrst_scalar::bytea))[1], 'hex'), '')` : `coalesce((array_agg(_t.pgrst_scalar::text))[1], '')`;
2636
+ };
2637
+ const GEOJSON_BODY = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_t)::json), '[]'::json))::text";
2638
+ const bodyExpr = (shape, strip, csv, handler, geojson) => {
2639
+ if (geojson && shape !== "none") return GEOJSON_BODY;
2640
+ if (handler && shape !== "none") return handlerBody(handler);
2641
+ if (csv && shape !== "none") return CSV_BODY;
2642
+ const wrap = (e) => strip ? `json_strip_nulls(${e})` : e;
2643
+ switch (shape) {
2644
+ case "array": return `coalesce(${wrap("json_agg(_t)")}, '[]'::json)::text`;
2645
+ case "object": return `coalesce((${wrap("json_agg(_t)")} -> 0), 'null'::json)::text`;
2646
+ case "scalar-array": return `coalesce(json_agg(_t.pgrst_scalar), '[]'::json)::text`;
2647
+ case "scalar": return `coalesce((json_agg(_t.pgrst_scalar) -> 0)::text, 'null')`;
2648
+ case "none": return `''::text`;
2649
+ }
2650
+ };
2651
+ /**
2652
+ * The one statement that runs a request. Result columns:
2653
+ * `page_total`, `body`, `total_result_set`, `response_headers`, `response_status`.
2654
+ *
2655
+ * `response.headers` / `response.status` are read at the end so that functions
2656
+ * (RPC, triggers) can influence the response by calling `set_config`.
2657
+ */
2658
+ const buildStatement = (o) => {
2659
+ const up = o.upsertColumns;
2660
+ return `WITH ${[...o.source && up ? [`pgrst_ins AS (${o.source})`, `pgrst_source AS (SELECT ${up.join(", ")} FROM pgrst_ins)`] : o.source ? [`pgrst_source AS (${o.source})`] : [], `pgrst_body AS (${o.rows})`].join(", ")} SELECT count(*)::text AS page_total, ${bodyExpr(o.shape, Boolean(o.stripNulls), Boolean(o.csv), o.handler, o.geojson)} AS body, ${o.count ? `(${o.count})::text` : "NULL::text"} AS total_result_set, nullif(current_setting('response.headers', true), '') AS response_headers, ` + (up ? `coalesce(nullif(current_setting('response.status', true), ''), CASE WHEN EXISTS (SELECT 1 FROM pgrst_ins) AND NOT EXISTS (SELECT 1 FROM pgrst_ins WHERE pgrst_inserted) THEN '200' END)` : `nullif(current_setting('response.status', true), '')`) + " AS response_status FROM pgrst_body AS _t";
2661
+ };
2662
+
2663
+ //#endregion
2664
+ //#region src/sql/write.ts
2665
+ const returningSql = (rel, returning) => {
2666
+ const alias = quoteIdent(ROOT_ALIAS);
2667
+ if (returning === "representation") return `${alias}.*`;
2668
+ if (returning === "pk" && rel.pk.length > 0) return rel.pk.map((c) => `${alias}.${quoteIdent(c)}`).join(", ");
2669
+ return "1";
2670
+ };
2671
+ const columnNotFound = (rel, column) => new PgRestError(400, "PGRST204", `Could not find the '${column}' column of '${rel.name}' in the schema cache`, null, null);
2672
+ const checkColumns = (rel, cols) => {
2673
+ const known = new Set(rel.columns.map((c) => c.name));
2674
+ for (const c of cols) if (!known.has(c)) throw columnNotFound(rel, c);
2675
+ };
2676
+ const unionKeys = (rows) => {
2677
+ const seen = /* @__PURE__ */ new Set();
2678
+ for (const r of rows) for (const k of Object.keys(r)) seen.add(k);
2679
+ return [...seen];
2680
+ };
2681
+ /**
2682
+ * `INSERT … RETURNING …`. The payload is `$1` (JSON text, an array of objects);
2683
+ * it is always cast through `$1::text` so the driver never re-encodes it.
2684
+ */
2685
+ const buildInsert = (o) => {
2686
+ const { rel } = o;
2687
+ const table = quoteQualified(rel.schema, rel.name);
2688
+ const target = `${table} AS ${quoteIdent(ROOT_ALIAS)}`;
2689
+ const returning = returningSql(rel, o.returning);
2690
+ const cols = o.columns ? [...o.columns] : unionKeys(o.rows);
2691
+ if (!o.columns && o.rows.some((r) => Object.keys(r).length !== cols.length)) throw new PgRestError(400, "PGRST102", "All object keys must match");
2692
+ checkColumns(rel, cols);
2693
+ if (o.rows.length === 0) {
2694
+ const first = rel.columns[0];
2695
+ if (!first) throw new PgRestError(400, "PGRST102", "Cannot insert into a relation without columns");
2696
+ return `INSERT INTO ${target} (${quoteIdent(first.name)}) SELECT NULL WHERE FALSE RETURNING ${returning}`;
2697
+ }
2698
+ if (cols.length === 0) {
2699
+ const first = rel.columns[0];
2700
+ if (!first) throw new PgRestError(400, "PGRST102", "Cannot insert into a relation without columns");
2701
+ const values = o.rows.map(() => "(DEFAULT)").join(", ");
2702
+ return `INSERT INTO ${target} (${quoteIdent(first.name)}) VALUES ${values || "(DEFAULT)"} RETURNING ${returning}`;
2703
+ }
2704
+ const colList = cols.map(quoteIdent).join(", ");
2705
+ const typed = `null::${table}`;
2706
+ const json = "($1::text)::json";
2707
+ let select;
2708
+ if (o.missingDefault) {
2709
+ const byName = new Map(rel.columns.map((c) => [c.name, c]));
2710
+ select = `SELECT ${cols.map((c) => {
2711
+ const allPresent = o.rows.every((r) => c in r);
2712
+ const value = `_p.${quoteIdent(c)}`;
2713
+ if (allPresent) return value;
2714
+ const dflt = byName.get(c)?.defaultExpr ?? "NULL";
2715
+ return `CASE WHEN (_e.value::jsonb) ? ${"'" + c.replace(/'/g, "''") + "'"} THEN ${value} ELSE ${dflt} END`;
2716
+ }).join(", ")} FROM json_array_elements(${json}) AS _e(value) CROSS JOIN LATERAL json_populate_record(${typed}, _e.value) AS _p`;
2717
+ } else select = `SELECT ${colList} FROM json_populate_recordset(${typed}, ${json})`;
2718
+ let conflict = "";
2719
+ if (o.resolution) {
2720
+ const keys = o.onConflict && o.onConflict.length > 0 ? o.onConflict : rel.pk;
2721
+ checkColumns(rel, keys);
2722
+ const targetCols = keys.length ? ` (${keys.map(quoteIdent).join(", ")})` : "";
2723
+ if (o.resolution === "ignore-duplicates") conflict = ` ON CONFLICT${targetCols} DO NOTHING`;
2724
+ else {
2725
+ if (keys.length === 0) throw new PgRestError(400, "PGRST100", "Upsert needs a conflict target", `"${rel.name}" has no primary key; pass on_conflict=<columns>`);
2726
+ conflict = ` ON CONFLICT${targetCols} DO UPDATE SET ${cols.map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`).join(", ")}`;
2727
+ }
2728
+ } else if (o.onConflict && o.onConflict.length > 0) {}
2729
+ const flag = o.flagInserted && o.resolution === "merge-duplicates" ? `, (${quoteIdent(ROOT_ALIAS)}.xmax = 0) AS pgrst_inserted` : "";
2730
+ return `INSERT INTO ${target} (${colList}) ${select}${conflict} RETURNING ${flag && returning === "1" ? "1 AS pgrst_one" : returning}${flag}`;
2731
+ };
2732
+ /** Columns of `buildInsert`'s output without the `pgrst_inserted` marker, for a merge-duplicates upsert. */
2733
+ const upsertColumns = (rel, returning) => {
2734
+ if (returning === "representation") return rel.columns.map((c) => quoteIdent(c.name));
2735
+ if (returning === "pk" && rel.pk.length > 0) return rel.pk.map(quoteIdent);
2736
+ return ["pgrst_one"];
2737
+ };
2738
+ /** `UPDATE … SET … RETURNING …`, or `null` when the payload sets no columns. */
2739
+ const buildUpdate = (o) => {
2740
+ const { rel } = o;
2741
+ const cols = Object.keys(o.body);
2742
+ checkColumns(rel, cols);
2743
+ if (cols.length === 0) return null;
2744
+ const table = quoteQualified(rel.schema, rel.name);
2745
+ const set = cols.map((c) => `${quoteIdent(c)} = _u.${quoteIdent(c)}`).join(", ");
2746
+ const where = whereSql(o.filters, ROOT_ALIAS, rel);
2747
+ return `UPDATE ${table} AS ${quoteIdent(ROOT_ALIAS)} SET ${set} FROM (SELECT * FROM json_populate_record(null::${table}, ($1::text)::json)) AS _u${where.length ? ` WHERE ${where.join(" AND ")}` : ""} RETURNING ${returningSql(rel, o.returning)}`;
2748
+ };
2749
+ const buildDelete = (o) => {
2750
+ const where = whereSql(o.filters, ROOT_ALIAS, o.rel);
2751
+ return `DELETE FROM ${quoteQualified(o.rel.schema, o.rel.name)} AS ${quoteIdent(ROOT_ALIAS)}${where.length ? ` WHERE ${where.join(" AND ")}` : ""} RETURNING ${returningSql(o.rel, o.returning)}`;
2752
+ };
2753
+
2754
+ //#endregion
2755
+ //#region src/core.ts
2756
+ /** Statement options that depend on the negotiated output format. */
2757
+ const formatOptions = (format) => ({
2758
+ csv: format.kind === "csv",
2759
+ geojson: format.kind === "geojson",
2760
+ handler: format.kind === "handler" ? format.choice : void 0
2761
+ });
2762
+ 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;
2768
+ };
2769
+ const parseCookies = (header) => {
2770
+ const out = {};
2771
+ if (!header) return out;
2772
+ for (const part of header.split(";")) {
2773
+ const eq = part.indexOf("=");
2774
+ if (eq < 0) continue;
2775
+ const k = part.slice(0, eq).trim();
2776
+ if (!k) continue;
2777
+ const v = part.slice(eq + 1).trim().replace(/^"|"$/g, "");
2778
+ try {
2779
+ out[k] = decodeURIComponent(v);
2780
+ } catch {
2781
+ out[k] = v;
2782
+ }
2783
+ }
2784
+ return out;
2785
+ };
2786
+ const applyCtxSql = (env, ctx, role, claims, request, method, pathname, schema, prefer) => {
2787
+ const entries = [["role", role], ["request.jwt.claims", JSON.stringify(claims)]];
2788
+ if (env.config.requestSettings) {
2789
+ const headers = {};
2790
+ request.headers.forEach((v, k) => {
2791
+ headers[k] = v;
2792
+ });
2793
+ entries.push(["request.method", method], ["request.path", pathname], ["request.headers", JSON.stringify(headers)], ["request.cookies", JSON.stringify(parseCookies(request.headers.get("cookie")))]);
2794
+ }
2795
+ const searchPath = [schema, ...env.config.extraSearchPath].map(quoteIdent).join(", ");
2796
+ entries.push(["search_path", searchPath]);
2797
+ if (prefer.timezone) entries.push(["timezone", prefer.timezone]);
2798
+ for (const [k, v] of Object.entries(ctx.settings ?? {})) {
2799
+ if (!/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z0-9_.]+$/.test(k)) throw new PgRestError(500, "PGRST100", `Invalid setting name "${k}"`, null, "Custom settings need a prefix, e.g. app.tenant_id");
2800
+ entries.push([k, v]);
2801
+ }
2802
+ return {
2803
+ sql: `SELECT ${entries.map(([k], i) => `set_config(${literal(k)}, $${i + 1}::text, true)`).join(", ")}`,
2804
+ params: entries.map(([, v]) => v)
2805
+ };
2806
+ };
2807
+ /** Parsed `response.headers` GUC: `[{"X-A": "b"}, ...]`. */
2808
+ const parseResponseHeaders = (raw) => {
2809
+ 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");
2811
+ let parsed;
2812
+ try {
2813
+ parsed = JSON.parse(String(raw));
2814
+ } catch {
2815
+ throw bad();
2816
+ }
2817
+ if (!Array.isArray(parsed)) throw bad();
2818
+ const out = {};
2819
+ for (const h of parsed) {
2820
+ if (!h || typeof h !== "object" || Array.isArray(h)) throw bad();
2821
+ const keys = Object.keys(h);
2822
+ if (keys.length !== 1) throw bad();
2823
+ const v = h[keys[0]];
2824
+ if (typeof v !== "string") throw bad();
2825
+ out[keys[0]] = v;
2826
+ }
2827
+ return out;
2828
+ };
2829
+ const parseStatusGuc = (raw) => {
2830
+ if (raw == null || raw === "") return void 0;
2831
+ const n = Number(raw);
2832
+ if (!Number.isInteger(n) || n < 100 || n > 599) throw new PgRestError(500, "PGRST112", "response.status guc must be a valid status code");
2833
+ return n;
2834
+ };
2835
+ const appliedHeader = (p, honoredTx) => {
2836
+ const parts = [];
2837
+ if (honoredTx && p.tx) parts.push(`tx=${p.tx}`);
2838
+ if (p.return) parts.push(`return=${p.return}`);
2839
+ if (p.count) parts.push(`count=${p.count}`);
2840
+ if (p.resolution) parts.push(`resolution=${p.resolution}`);
2841
+ if (p.missing) parts.push(`missing=${p.missing}`);
2842
+ if (p.handling) parts.push(`handling=${p.handling}`);
2843
+ if (p.timezone) parts.push(`timezone=${p.timezone}`);
2844
+ if (p.maxAffected !== void 0 && p.handling === "strict") parts.push(`max-affected=${p.maxAffected}`);
2845
+ return parts.length ? parts.join(", ") : void 0;
2846
+ };
2847
+ var RollbackWith = class extends Error {
2848
+ constructor(value) {
2849
+ super("rollback");
2850
+ this.value = value;
2851
+ }
2852
+ };
2853
+ const asRows = (body) => {
2854
+ const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
2855
+ if (Array.isArray(body)) {
2856
+ if (!body.every(isObj)) throw new PgRestError(400, "PGRST102", "All object keys must match");
2857
+ return body;
2858
+ }
2859
+ return isObj(body) ? [body] : [];
2860
+ };
2861
+ const effectivePaging = (env, query, range) => {
2862
+ if (query.root.limit !== void 0 && query.root.limit < 0) throw new PgRestError(416, "PGRST103", "Requested range not satisfiable", "Limit should be greater than or equal to zero.", null);
2863
+ let limit = query.root.limit ?? range?.limit;
2864
+ const offset = query.root.offset ?? range?.offset ?? 0;
2865
+ const max = env.config.maxRows;
2866
+ if (max !== void 0) limit = limit === void 0 ? max : Math.min(limit, max);
2867
+ return {
2868
+ limit,
2869
+ offset
2870
+ };
2871
+ };
2872
+ 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);
2874
+ if (!isSupportedMethod(request.method)) return new Response(null, {
2875
+ status: 405,
2876
+ headers: { "Content-Type": JSON_CT }
2877
+ });
2878
+ const method = parseMethod(request.method);
2879
+ const url = new URL(request.url);
2880
+ const target = parseTarget(url.pathname, env.config.basePath);
2881
+ const schema = readSchema(env, request, method);
2882
+ const prefer = parsePrefer(request.headers.get("prefer"));
2883
+ const invalid = [...prefer.invalid];
2884
+ const honoredTx = env.config.allowTxOverride;
2885
+ if (prefer.tx && !honoredTx) invalid.push(`tx=${prefer.tx}`);
2886
+ if (prefer.handling === "strict" && invalid.length > 0) throw new PgRestError(400, "PGRST122", "Invalid preferences given with handling=strict", `Invalid preferences: ${invalid.join(", ")}`);
2887
+ if (method === "OPTIONS") {
2888
+ const allow = target.type === "root" ? "OPTIONS,GET,HEAD" : target.type === "rpc" ? "OPTIONS,GET,HEAD,POST" : (() => {
2889
+ const rel = getRelation(env.cache, schema, target.name);
2890
+ const m = [
2891
+ "OPTIONS",
2892
+ "GET",
2893
+ "HEAD"
2894
+ ];
2895
+ if (rel.insertable) m.push("POST");
2896
+ if (rel.insertable && rel.updatable) m.push("PUT");
2897
+ if (rel.updatable) m.push("PATCH");
2898
+ if (rel.deletable) m.push("DELETE");
2899
+ return m.join(",");
2900
+ })();
2901
+ return new Response(null, {
2902
+ status: 200,
2903
+ headers: { Allow: allow }
2904
+ });
2905
+ }
2906
+ if (target.type === "root") {
2907
+ if (method !== "GET" && method !== "HEAD") throw new PgRestError(405, "PGRST117", `Unsupported HTTP method: ${method}`);
2908
+ if (!env.config.openApi) throw new PgRestError(404, "PGRST126", "Root endpoint metadata is disabled");
2909
+ parseAccept(request.headers.get("accept"), true);
2910
+ const doc = buildOpenApi(env.cache, schema, {
2911
+ serverUrl: env.config.serverUrl ?? url.origin,
2912
+ basePath: env.config.basePath
2913
+ });
2914
+ return new Response(method === "HEAD" ? null : JSON.stringify(doc), {
2915
+ status: 200,
2916
+ headers: { "Content-Type": "application/openapi+json; charset=utf-8" }
2917
+ });
2918
+ }
2919
+ const requested = parseAccept(request.headers.get("accept"), false, env.config.planEnabled, mediaChoices(env.cache, schema, target, url.searchParams.has("select")));
2920
+ const format = requested.kind === "plan" ? requested.inner : requested;
2921
+ const wantsPlan = requested.kind === "plan" ? requested : void 0;
2922
+ const range = parseRange(request.headers.get("range"), request.headers.get("range-unit"));
2923
+ const body = await parseBody(request, method);
2924
+ 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) => {
2930
+ const rel = getRelation(env.cache, schema, name);
2931
+ const query = parseQuery([...url.searchParams]);
2932
+ const strip = format.kind === "json" || format.kind === "object" ? format.stripNulls : false;
2933
+ const singular = format.kind === "object";
2934
+ if (method === "GET" || method === "HEAD") {
2935
+ const { limit, offset } = effectivePaging(env, query, range);
2936
+ const read = buildRead({
2937
+ cache: env.cache,
2938
+ rel,
2939
+ from: `${quoteIdent(rel.schema)}.${quoteIdent(rel.name)}`,
2940
+ query,
2941
+ limit,
2942
+ offset
2943
+ });
2944
+ const shape$1 = singular ? "object" : "array";
2945
+ const paged = limit !== void 0 || offset > 0;
2946
+ const needCount = prefer.count === "exact" || prefer.count === "estimated";
2947
+ return {
2948
+ method,
2949
+ kind: "read",
2950
+ readOnly: true,
2951
+ source: false,
2952
+ params: [],
2953
+ shape: shape$1,
2954
+ rel,
2955
+ offset,
2956
+ limit,
2957
+ pagedRead: paged,
2958
+ countInStatement: needCount && paged,
2959
+ planned: prefer.count === "planned" || prefer.count === "estimated" ? read.unpaged : void 0,
2960
+ statement: buildStatement({
2961
+ rows: read.rows,
2962
+ count: needCount && paged ? read.count : void 0,
2963
+ shape: shape$1,
2964
+ stripNulls: strip,
2965
+ ...formatOptions(format)
2966
+ })
2967
+ };
2968
+ }
2969
+ const returning = prefer.return === "representation" ? "representation" : prefer.return === "headers-only" ? "pk" : "minimal";
2970
+ const shapeFor = (r) => r === "representation" ? singular ? "object" : "array" : r === "pk" ? "array" : "none";
2971
+ const rowsFor = (r) => {
2972
+ if (r === "representation") {
2973
+ const resultQuery = {
2974
+ ...query,
2975
+ root: {
2976
+ ...query.root,
2977
+ filters: [],
2978
+ presence: []
2979
+ }
2980
+ };
2981
+ return {
2982
+ rows: buildRead({
2983
+ cache: env.cache,
2984
+ rel,
2985
+ from: "pgrst_source",
2986
+ query: resultQuery
2987
+ }).rows,
2988
+ shape: shapeFor(r)
2989
+ };
2990
+ }
2991
+ return {
2992
+ rows: "SELECT * FROM pgrst_source",
2993
+ shape: shapeFor(r)
2994
+ };
2995
+ };
2996
+ const common = {
2997
+ method,
2998
+ rel,
2999
+ readOnly: false,
3000
+ source: true,
3001
+ offset: 0,
3002
+ countInStatement: false,
3003
+ pagedRead: false
3004
+ };
3005
+ if (method === "POST") {
3006
+ if (!body) throw new PgRestError(400, "PGRST102", "Empty or invalid json");
3007
+ const rows = asRows(body.value);
3008
+ const insertSql = buildInsert({
3009
+ rel,
3010
+ rows,
3011
+ columns: query.columns,
3012
+ onConflict: query.onConflict,
3013
+ resolution: prefer.resolution,
3014
+ missingDefault: prefer.missing === "default",
3015
+ flagInserted: true,
3016
+ returning
3017
+ });
3018
+ const { rows: outRows$1, shape: shape$1 } = rowsFor(returning);
3019
+ return {
3020
+ ...common,
3021
+ kind: "insert",
3022
+ returning,
3023
+ params: insertSql.includes("$1") ? [JSON.stringify(rows)] : [],
3024
+ shape: shape$1,
3025
+ statement: buildStatement({
3026
+ source: insertSql,
3027
+ rows: outRows$1,
3028
+ shape: shape$1,
3029
+ stripNulls: strip,
3030
+ ...formatOptions(format),
3031
+ upsertColumns: insertSql.includes("pgrst_inserted") ? upsertColumns(rel, returning) : void 0
3032
+ })
3033
+ };
3034
+ }
3035
+ if (method === "PATCH") {
3036
+ if (!body) throw new PgRestError(400, "PGRST102", "Empty or invalid json");
3037
+ const first = Array.isArray(body.value) ? body.value[0] : body.value;
3038
+ const obj$1 = first !== null && typeof first === "object" && !Array.isArray(first) ? first : {};
3039
+ const updateSql = buildUpdate({
3040
+ rel,
3041
+ body: obj$1,
3042
+ filters: query.root.filters,
3043
+ returning
3044
+ });
3045
+ const { rows: outRows$1, shape: shape$1 } = rowsFor(returning);
3046
+ if (updateSql === null) {
3047
+ const emptyShape = returning === "representation" ? singular ? "object" : "array" : "none";
3048
+ return {
3049
+ ...common,
3050
+ kind: "update",
3051
+ returning,
3052
+ params: [],
3053
+ shape: emptyShape,
3054
+ statement: buildStatement({
3055
+ source: "SELECT 1 WHERE FALSE",
3056
+ rows: "SELECT * FROM pgrst_source",
3057
+ shape: emptyShape,
3058
+ ...formatOptions(format)
3059
+ })
3060
+ };
3061
+ }
3062
+ return {
3063
+ ...common,
3064
+ kind: "update",
3065
+ returning,
3066
+ params: [JSON.stringify(obj$1)],
3067
+ shape: shape$1,
3068
+ statement: buildStatement({
3069
+ source: updateSql,
3070
+ rows: outRows$1,
3071
+ shape: shape$1,
3072
+ stripNulls: strip,
3073
+ ...formatOptions(format)
3074
+ })
3075
+ };
3076
+ }
3077
+ if (method === "DELETE") {
3078
+ const deleteSql = buildDelete({
3079
+ rel,
3080
+ filters: query.root.filters,
3081
+ returning
3082
+ });
3083
+ const { rows: outRows$1, shape: shape$1 } = rowsFor(returning);
3084
+ return {
3085
+ ...common,
3086
+ kind: "delete",
3087
+ returning,
3088
+ params: [],
3089
+ shape: shape$1,
3090
+ statement: buildStatement({
3091
+ source: deleteSql,
3092
+ rows: outRows$1,
3093
+ shape: shape$1,
3094
+ stripNulls: strip,
3095
+ ...formatOptions(format)
3096
+ })
3097
+ };
3098
+ }
3099
+ if (!body) throw new PgRestError(400, "PGRST102", "Empty or invalid json");
3100
+ const row = body.value;
3101
+ if (Array.isArray(row) && !row.every((r) => r !== null && typeof r === "object" && !Array.isArray(r))) throw new PgRestError(400, "PGRST102", "All object keys must match");
3102
+ if (row === null || typeof row !== "object" || Array.isArray(row)) throw new PgRestError(400, "PGRST102", "Empty or invalid json", "The payload of a PUT request must be a JSON object");
3103
+ if (query.root.limit !== void 0 || query.root.offset !== void 0) throw new PgRestError(400, "PGRST114", "limit/offset querystring parameters are not allowed for PUT");
3104
+ if (rel.pk.length === 0) throw new PgRestError(405, "PGRST105", "Filters must include all and only primary key columns with 'eq' operators");
3105
+ const keyValues = /* @__PURE__ */ new Map();
3106
+ for (const f of query.root.filters) if (f.kind === "filter" && f.op === "eq" && !f.negate && !f.quantifier && f.field.json.length === 0 && typeof f.value === "string" && rel.pk.includes(f.field.name)) keyValues.set(f.field.name, f.value);
3107
+ else throw new PgRestError(405, "PGRST105", "Filters must include all and only primary key columns with 'eq' operators");
3108
+ if (!rel.pk.every((c) => keyValues.has(c))) throw new PgRestError(405, "PGRST105", "Filters must include all and only primary key columns with 'eq' operators");
3109
+ const obj = row;
3110
+ for (const c of rel.pk) if (c in obj && String(obj[c]) !== keyValues.get(c)) throw new PgRestError(400, "PGRST115", "Payload values do not match URL in primary key column(s)");
3111
+ const full = {
3112
+ ...Object.fromEntries(rel.pk.map((c) => [c, keyValues.get(c)])),
3113
+ ...obj
3114
+ };
3115
+ const putSql = buildInsert({
3116
+ rel,
3117
+ rows: [full],
3118
+ onConflict: rel.pk,
3119
+ resolution: "merge-duplicates",
3120
+ returning
3121
+ });
3122
+ const { rows: outRows, shape } = rowsFor(returning);
3123
+ return {
3124
+ ...common,
3125
+ kind: "put",
3126
+ returning,
3127
+ params: [JSON.stringify([full])],
3128
+ shape,
3129
+ statement: buildStatement({
3130
+ source: putSql,
3131
+ rows: outRows,
3132
+ shape,
3133
+ stripNulls: strip,
3134
+ ...formatOptions(format)
3135
+ })
3136
+ };
3137
+ };
3138
+ /** A query-string value in the filter syntax (`eq.5`, `not.in.(1,2)`, `like(any).{a*}`), not a plain argument. */
3139
+ 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) => {
3141
+ if (method !== "GET" && method !== "HEAD" && method !== "POST") throw new PgRestError(405, "PGRST101", `Cannot use the ${method} method on RPC`);
3142
+ const RESERVED = new Set([
3143
+ "select",
3144
+ "order",
3145
+ "limit",
3146
+ "offset",
3147
+ "and",
3148
+ "or",
3149
+ "columns",
3150
+ "on_conflict"
3151
+ ]);
3152
+ let argValues = {};
3153
+ let nonObjectBody = false;
3154
+ const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
3155
+ const getArgKeys = /* @__PURE__ */ new Set();
3156
+ const bare = /* @__PURE__ */ new Set();
3157
+ if (method === "POST") {
3158
+ if (body) {
3159
+ const v = body.value;
3160
+ if (isObj(v)) argValues = v;
3161
+ else {
3162
+ nonObjectBody = true;
3163
+ if (Array.isArray(v) && v.length > 0) {
3164
+ const keysOf = (o) => isObj(o) ? Object.keys(o).sort().join("\0") : void 0;
3165
+ const first = keysOf(v[0]);
3166
+ if (first === void 0 || v.some((o) => keysOf(o) !== first)) throw new PgRestError(400, "PGRST102", "All object keys must match", null, null);
3167
+ argValues = v[0];
3168
+ }
3169
+ }
3170
+ }
3171
+ } else {
3172
+ for (const kv of url.search.slice(1).split("&")) if (kv !== "" && !kv.includes("=")) bare.add(decodeURIComponent(kv));
3173
+ const lists = /* @__PURE__ */ new Map();
3174
+ for (const [k, v] of url.searchParams) {
3175
+ if (RESERVED.has(k) || bare.has(k) || looksLikeFilter(v) || k.includes(".")) continue;
3176
+ lists.set(k, [...lists.get(k) ?? [], v]);
3177
+ }
3178
+ for (const [k, vs] of lists) {
3179
+ getArgKeys.add(k);
3180
+ argValues[k] = vs.length === 1 ? vs[0] : vs;
3181
+ }
3182
+ }
3183
+ const provided = Object.keys(argValues);
3184
+ const fn = resolveFunction(env.cache, schema, name, new Set(provided), nonObjectBody, method === "POST");
3185
+ for (const a of fn.args) if (a.variadic && getArgKeys.has(a.name) && !Array.isArray(argValues[a.name])) argValues[a.name] = [argValues[a.name]];
3186
+ const rawBody = nonObjectBody && isSingleUnnamedParam(fn);
3187
+ const call = buildRpc(env.cache, fn, provided, rawBody);
3188
+ 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) });
3190
+ const argsJson = rawBody ? JSON.stringify(body.value) : JSON.stringify(argValues);
3191
+ const params = call.usesBody ? [rawBody && body.kind === "json" && typeof body.value === "string" ? body.value : argsJson] : [];
3192
+ const strip = format.kind === "json" || format.kind === "object" ? format.stripNulls : false;
3193
+ const base = {
3194
+ method,
3195
+ kind: "rpc",
3196
+ readOnly: method !== "POST",
3197
+ source: false,
3198
+ rpc: call,
3199
+ rel: call.relation,
3200
+ params,
3201
+ offset: 0,
3202
+ countInStatement: false,
3203
+ pagedRead: false
3204
+ };
3205
+ if (call.shape === "rows" || call.shape === "object") {
3206
+ const { limit, offset } = effectivePaging(env, query, range);
3207
+ const read = buildRead({
3208
+ cache: env.cache,
3209
+ rel: call.relation,
3210
+ from: `(${call.source})`,
3211
+ query,
3212
+ limit,
3213
+ offset
3214
+ });
3215
+ const shape$1 = call.shape === "object" || format.kind === "object" ? "object" : "array";
3216
+ const paged = limit !== void 0 || offset > 0;
3217
+ const needCount = prefer.count === "exact" || prefer.count === "estimated";
3218
+ return {
3219
+ ...base,
3220
+ offset,
3221
+ limit,
3222
+ pagedRead: paged,
3223
+ countInStatement: needCount && paged,
3224
+ planned: prefer.count === "planned" || prefer.count === "estimated" ? read.unpaged : void 0,
3225
+ shape: shape$1,
3226
+ statement: buildStatement({
3227
+ rows: read.rows,
3228
+ count: needCount && paged ? read.count : void 0,
3229
+ shape: shape$1,
3230
+ stripNulls: strip,
3231
+ ...formatOptions(format)
3232
+ })
3233
+ };
3234
+ }
3235
+ const shape = call.shape === "void" && format.kind !== "geojson" ? "none" : call.shape === "scalar" ? "scalar" : "scalar-array";
3236
+ return {
3237
+ ...base,
3238
+ shape,
3239
+ statement: buildStatement({
3240
+ rows: call.source,
3241
+ shape,
3242
+ ...formatOptions(format)
3243
+ })
3244
+ };
3245
+ };
3246
+ const execute = async (env, ctx, request, method, pathname, schema, prefer, format, plan, honoredTx, wantsPlan) => {
3247
+ const role = ctx.role ?? env.config.anonymousRole;
3248
+ const claims = {
3249
+ role,
3250
+ ...ctx.claims ?? {}
3251
+ };
3252
+ const setup = applyCtxSql(env, ctx, role, claims, request, method, pathname, schema, prefer);
3253
+ const strict = prefer.handling === "strict";
3254
+ const rollback = honoredTx && prefer.tx === "rollback" || wantsPlan !== void 0;
3255
+ const attempt = async () => {
3256
+ try {
3257
+ return await env.db.transaction(async (s) => {
3258
+ if (plan.readOnly) await s.query("SET TRANSACTION READ ONLY");
3259
+ await s.query(setup.sql, setup.params);
3260
+ const pre = env.config.preRequest;
3261
+ if (typeof pre === "string") await s.query(`SELECT ${pre.split(".").map(quoteIdent).join(".")}()`);
3262
+ else if (pre) await pre(s, {
3263
+ role,
3264
+ claims,
3265
+ method,
3266
+ path: pathname,
3267
+ request
3268
+ });
3269
+ let planned;
3270
+ if (plan.planned) {
3271
+ const cell = (await s.query(`EXPLAIN (FORMAT JSON) ${plan.planned}`))[0]?.["QUERY PLAN"];
3272
+ const parsed = typeof cell === "string" ? JSON.parse(cell) : cell;
3273
+ const n = Number(parsed?.[0]?.Plan?.["Plan Rows"]);
3274
+ planned = Number.isFinite(n) ? n : void 0;
3275
+ }
3276
+ if (wantsPlan) {
3277
+ const opts = ["FORMAT " + wantsPlan.format.toUpperCase(), ...wantsPlan.options.map((o) => o.toUpperCase())];
3278
+ const cells = (await s.query(`EXPLAIN (${opts.join(", ")}) ${plan.statement}`, plan.params)).map((l) => l["QUERY PLAN"]);
3279
+ throw new RollbackWith({
3280
+ pageTotal: 0,
3281
+ body: wantsPlan.format === "json" ? typeof cells[0] === "string" ? cells[0] : JSON.stringify(cells[0], null, 2) : cells.join("\n"),
3282
+ total: void 0,
3283
+ planned: void 0,
3284
+ responseHeaders: {},
3285
+ responseStatus: void 0
3286
+ });
3287
+ }
3288
+ const row = (await s.query(plan.statement, plan.params))[0] ?? {};
3289
+ const pageTotal = Number(row["page_total"] ?? 0);
3290
+ const raw = {
3291
+ pageTotal,
3292
+ body: String(row["body"] ?? ""),
3293
+ total: row["total_result_set"] == null ? void 0 : Number(row["total_result_set"]),
3294
+ planned,
3295
+ responseHeaders: parseResponseHeaders(row["response_headers"]),
3296
+ responseStatus: parseStatusGuc(row["response_status"])
3297
+ };
3298
+ if (format.kind === "object" && plan.shape === "object" && plan.rpc?.shape !== "object" && pageTotal !== 1) throw new PgRestError(406, "PGRST116", "Cannot coerce the result to a single JSON object", `The result contains ${pageTotal} rows`);
3299
+ if (strict && prefer.maxAffected !== void 0) {
3300
+ if (plan.kind === "rpc" && plan.rpc && !plan.rpc.fn.returnsSet) throw new PgRestError(400, "PGRST128", "max-affected preference is only valid for functions that return a set", null, null);
3301
+ if ((plan.kind === "update" || plan.kind === "delete" || plan.kind === "rpc") && pageTotal > prefer.maxAffected) throw new PgRestError(400, "PGRST124", "Query result exceeds max-affected preference constraint", `The query affects ${pageTotal} rows`);
3302
+ }
3303
+ if (rollback) throw new RollbackWith(raw);
3304
+ return raw;
3305
+ });
3306
+ } catch (e) {
3307
+ if (e instanceof RollbackWith) return e.value;
3308
+ if (e instanceof DbQueryError && plan.kind !== "rpc" && plan.rel) {
3309
+ const fix = (t) => t?.replace(/"pgrst_root"/g, `"${plan.rel.name}"`).replace(/pgrst_root\./g, `${plan.rel.name}.`);
3310
+ throw new DbQueryError(e.sqlState, fix(e.message) ?? e.message, fix(e.detail), fix(e.hint));
3311
+ }
3312
+ throw e;
3313
+ }
3314
+ };
3315
+ let result;
3316
+ for (let i = 0;; i++) try {
3317
+ result = await attempt();
3318
+ break;
3319
+ } catch (e) {
3320
+ if (e instanceof DbQueryError && e.sqlState === "40001" && i < 2) continue;
3321
+ throw e;
3322
+ }
3323
+ if (wantsPlan) {
3324
+ const opts = wantsPlan.options.length ? `; options=${wantsPlan.options.join("|")}` : "";
3325
+ return new Response(method === "HEAD" ? null : result.body, {
3326
+ status: 200,
3327
+ headers: {
3328
+ "Content-Type": `application/vnd.pgrst.plan+${wantsPlan.format}; for="${wantsPlan.forType}"${opts}; charset=utf-8`,
3329
+ Vary: "Accept, Prefer, Range"
3330
+ }
3331
+ });
3332
+ }
3333
+ return buildResponse(env, request, method, prefer, format, plan, result, honoredTx);
3334
+ };
3335
+ const buildResponse = (env, request, method, prefer, format, plan, r, honoredTx) => {
3336
+ const headers = { Vary: "Accept, Prefer, Range" };
3337
+ const applied = appliedHeader(prefer, honoredTx);
3338
+ if (applied) headers["Preference-Applied"] = applied;
3339
+ let total;
3340
+ if (prefer.count === "exact") total = plan.countInStatement ? r.total : r.pageTotal;
3341
+ else if (prefer.count === "planned") total = r.planned;
3342
+ else if (prefer.count === "estimated") {
3343
+ const max = env.config.maxRows;
3344
+ total = max !== void 0 && r.planned !== void 0 && r.planned > max ? r.planned : plan.countInStatement ? r.total : r.pageTotal;
3345
+ }
3346
+ const totalStr = total === void 0 ? "*" : String(total);
3347
+ let status = 200;
3348
+ let text = r.body;
3349
+ let contentType = JSON_CT;
3350
+ if (plan.kind === "read" || plan.kind === "rpc" && plan.pagedRead !== void 0 && (plan.shape === "array" || plan.shape === "object")) {
3351
+ const first = plan.offset;
3352
+ const last = plan.offset + r.pageTotal - 1;
3353
+ headers["Content-Range"] = r.pageTotal > 0 ? `${first}-${last}/${totalStr}` : `*/${totalStr}`;
3354
+ if (total !== void 0 && plan.offset > 0 && r.pageTotal === 0 && plan.offset >= total && prefer.count !== "planned") throw new PgRestError(416, "PGRST103", "Requested range not satisfiable", `An offset of ${plan.offset} was requested, but there are only ${total} rows.`, null, {
3355
+ "Content-Range": `*/${total}`,
3356
+ ...applied ? { "Preference-Applied": applied } : {}
3357
+ });
3358
+ if (total !== void 0 && r.pageTotal > 0 && r.pageTotal < total) status = 206;
3359
+ if (total !== void 0 && r.pageTotal === total && plan.offset > 0) status = 206;
3360
+ } else if (plan.kind === "rpc") {
3361
+ status = plan.shape === "none" ? 204 : 200;
3362
+ headers["Content-Range"] = r.pageTotal > 0 ? `0-${r.pageTotal - 1}/${totalStr}` : `*/${totalStr}`;
3363
+ } else {
3364
+ if (plan.kind === "update") {
3365
+ const n = r.pageTotal;
3366
+ headers["Content-Range"] = n > 0 ? `0-${n - 1}/${prefer.count === "exact" ? String(n) : "*"}` : `*/${prefer.count === "exact" ? "0" : "*"}`;
3367
+ } else if (plan.kind !== "put") headers["Content-Range"] = `*/${prefer.count === "exact" ? String(r.pageTotal) : "*"}`;
3368
+ const ret = plan.returning ?? "minimal";
3369
+ if (plan.kind === "insert") {
3370
+ status = 201;
3371
+ if (ret === "pk") {
3372
+ const rows = JSON.parse(r.body || "[]");
3373
+ const pk = plan.rel?.pk ?? [];
3374
+ if (rows.length === 1 && pk.length > 0) {
3375
+ const q = pk.map((c) => `${encodeURIComponent(c)}=eq.${encodeURIComponent(String(rows[0][c]))}`).join("&");
3376
+ headers["Location"] = `/${encodeURIComponent(plan.rel.name)}?${q}`;
3377
+ }
3378
+ }
3379
+ } else if (plan.kind === "put") status = ret === "representation" ? 200 : 204;
3380
+ else status = ret === "representation" ? 200 : 204;
3381
+ if (ret !== "representation") text = "";
3382
+ else if (plan.shape === "none") text = "";
3383
+ }
3384
+ if (plan.kind === "insert" && (plan.returning ?? "minimal") !== "representation") text = "";
3385
+ if (format.kind === "handler") contentType = format.choice.contentType;
3386
+ else if (format.kind === "csv") contentType = "text/csv; charset=utf-8";
3387
+ else if (format.kind === "geojson") contentType = "application/geo+json; charset=utf-8";
3388
+ else if (format.kind === "object") contentType = `application/vnd.pgrst.object+json${format.stripNulls ? ";nulls=stripped" : ""}; charset=utf-8`;
3389
+ else if (format.kind === "json" && format.stripNulls) contentType = "application/vnd.pgrst.array+json;nulls=stripped; charset=utf-8";
3390
+ Object.assign(headers, r.responseHeaders);
3391
+ if (r.responseStatus !== void 0) status = r.responseStatus;
3392
+ const noBody = status === 204 || status === 304 || text === "" || method === "HEAD";
3393
+ if (!(status === 204 || status === 304 || text === "" && method !== "HEAD" && !(format.kind === "handler" && plan.kind === "read"))) headers["Content-Type"] = contentType;
3394
+ if (!noBody && format.kind === "handler" && format.choice.bytea) return new Response(Buffer.from(text, "hex"), {
3395
+ status,
3396
+ headers
3397
+ });
3398
+ return new Response(noBody ? null : text, {
3399
+ status,
3400
+ headers
3401
+ });
3402
+ };
3403
+
3404
+ //#endregion
3405
+ //#region src/service.ts
3406
+ /** The schema could not be read from the database catalog. */
3407
+ var SchemaLoadError = class extends Error {
3408
+ _tag = "SchemaLoadError";
3409
+ constructor(cause) {
3410
+ super(`pg-rest: could not load the database schema: ${cause instanceof Error ? cause.message : String(cause)}`);
3411
+ this.cause = cause;
3412
+ this.name = "SchemaLoadError";
3413
+ }
3414
+ };
3415
+ const createSchemaStore = async (db, schemas) => {
3416
+ const load = () => introspect(db, schemas).catch((cause) => {
3417
+ throw new SchemaLoadError(cause);
3418
+ });
3419
+ let cache = await load();
3420
+ let queue = Promise.resolve();
3421
+ return {
3422
+ current: () => cache,
3423
+ reload() {
3424
+ const run$2 = queue.then(async () => {
3425
+ try {
3426
+ cache = await load();
3427
+ return cache;
3428
+ } catch (e) {
3429
+ console.warn("pg-rest: schema cache reload failed, keeping the previous cache", e.cause);
3430
+ throw e;
3431
+ }
3432
+ });
3433
+ queue = run$2.catch(() => void 0);
3434
+ return run$2;
3435
+ }
3436
+ };
3437
+ };
3438
+ const CONNECTION_ERROR = /^(ERR_POSTGRES_(CONNECTION|AUTH|TLS|IDLE|LIFETIME)|ECONN|ETIMEDOUT|EPIPE|ENOTFOUND)/;
3439
+ /** Map anything thrown while handling a request to an {@link ApiError}. */
3440
+ const toApiError = (e, role, anonymousRole) => {
3441
+ if (e instanceof ApiError) return e;
3442
+ if (e instanceof PgRestError) return new ApiError({
3443
+ status: e.status,
3444
+ body: e.body,
3445
+ headers: e.headers
3446
+ });
3447
+ if (e instanceof DbQueryError) return dbErrorToApiError(e, role, anonymousRole);
3448
+ const code = e?.code;
3449
+ if (typeof code === "string" && CONNECTION_ERROR.test(code)) return new ApiError({
3450
+ status: 503,
3451
+ headers: {},
3452
+ body: {
3453
+ code: "PGRST001",
3454
+ message: "Database connection failed",
3455
+ details: e instanceof Error ? e.message : null,
3456
+ hint: null
3457
+ }
3458
+ });
3459
+ return new ApiError({
3460
+ status: 500,
3461
+ headers: {},
3462
+ body: {
3463
+ code: "PGRSTX00",
3464
+ message: "Internal error",
3465
+ details: null,
3466
+ hint: null
3467
+ }
3468
+ });
3469
+ };
3470
+ const errorResponse = (err) => new Response(JSON.stringify(err.body), {
3471
+ status: err.status,
3472
+ headers: {
3473
+ "Content-Type": "application/json; charset=utf-8",
3474
+ ...err.status === 401 ? { "WWW-Authenticate": "Bearer" } : {},
3475
+ ...err.headers
3476
+ }
3477
+ });
3478
+ /** Handle a request. Never rejects: errors become PostgREST-style JSON responses. */
3479
+ const handleSafely = async (env, request, ctx) => {
3480
+ try {
3481
+ return await handleRequest(env, request, ctx);
3482
+ } catch (e) {
3483
+ const api = toApiError(e, ctx.role ?? env.config.anonymousRole, env.config.anonymousRole);
3484
+ if (api.status >= 500 && !(e instanceof PgRestError) && !(e instanceof DbQueryError)) console.error("pg-rest: unexpected error while handling a request", e);
3485
+ return errorResponse(api);
3486
+ }
3487
+ };
3488
+
3489
+ //#endregion
3490
+ //#region src/index.ts
3491
+ const UNEXPOSABLE = new Set(["pg_catalog", "information_schema"]);
3492
+ /** Create a PostgREST-compatible handler over a Postgres database. */
3493
+ const createPgRest = async (options) => {
3494
+ const schemas = options.schemas?.length ? [...options.schemas] : ["public"];
3495
+ for (const s of schemas) if (UNEXPOSABLE.has(s)) throw new Error(`pg-rest: the schema "${s}" cannot be exposed`);
3496
+ const user = options.db;
3497
+ const adapter = {
3498
+ async query(text, params) {
3499
+ try {
3500
+ return await user.query(text, params);
3501
+ } catch (e) {
3502
+ throw normalizeDbError(e);
3503
+ }
3504
+ },
3505
+ async transaction(fn) {
3506
+ try {
3507
+ return await user.transaction(fn);
3508
+ } catch (e) {
3509
+ throw normalizeDbError(e);
3510
+ }
3511
+ }
3512
+ };
3513
+ const config = {
3514
+ schemas,
3515
+ anonymousRole: options.anonymousRole === false ? void 0 : options.anonymousRole ?? "anon",
3516
+ maxRows: options.maxRows,
3517
+ basePath: options.basePath ?? "",
3518
+ extraSearchPath: options.extraSearchPath ?? ["public"],
3519
+ allowTxOverride: options.allowTxOverride ?? false,
3520
+ planEnabled: options.planEnabled ?? false,
3521
+ openApi: options.openApi ?? true,
3522
+ requestSettings: options.requestSettings ?? true,
3523
+ preRequest: options.preRequest,
3524
+ serverUrl: options.serverUrl
3525
+ };
3526
+ const store = await createSchemaStore(adapter, schemas);
3527
+ const handle = async (request, ctx) => {
3528
+ let context = ctx;
3529
+ if (!context && options.resolveContext) try {
3530
+ context = await options.resolveContext(request);
3531
+ } catch (e) {
3532
+ return errorResponse(e instanceof ApiError ? e : toApiError(e, config.anonymousRole, config.anonymousRole));
3533
+ }
3534
+ return handleSafely({
3535
+ db: adapter,
3536
+ config,
3537
+ cache: store.current()
3538
+ }, request, context ?? {});
3539
+ };
3540
+ return {
3541
+ handle,
3542
+ reloadSchema: async () => void await store.reload(),
3543
+ schemaCache: async () => store.current(),
3544
+ close: async () => {}
3545
+ };
3546
+ };
3547
+
3548
+ //#endregion
3549
+ export { ApiError, DbQueryError, PgRestError, SchemaLoadError, createPgRest };