@adrdp/pg-rest 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,669 @@
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
+ /**
26
+ * A one-line summary for stack traces and logs. The wire format is `body`, which this never
27
+ * touches: PostgREST clients read `message` from the JSON, while a program that catches the
28
+ * error reads it from `Error.message` and wants the code and hint with it.
29
+ */
30
+ const describe$1 = (b) => [
31
+ `${b.code}:`,
32
+ b.message,
33
+ b.details ? `(${b.details})` : "",
34
+ b.hint ? `hint: ${b.hint}` : ""
35
+ ].filter(Boolean).join(" ");
36
+ /** An error that has already been mapped to an HTTP response. */
37
+ var ApiError = class extends Error {
38
+ _tag = "ApiError";
39
+ status;
40
+ body;
41
+ headers;
42
+ constructor(fields) {
43
+ super(describe$1(fields.body));
44
+ this.name = "ApiError";
45
+ this.status = fields.status;
46
+ this.body = fields.body;
47
+ this.headers = fields.headers;
48
+ }
49
+ };
50
+ /** Normalized error thrown by a {@link DbAdapter}. */
51
+ var DbQueryError = class extends Error {
52
+ constructor(sqlState, message, detail, hint) {
53
+ super(message);
54
+ this.sqlState = sqlState;
55
+ this.detail = detail;
56
+ this.hint = hint;
57
+ this.name = "DbQueryError";
58
+ }
59
+ };
60
+ const SQLSTATE = /^[0-9A-Z]{5}$/;
61
+ /**
62
+ * Recognise a database error thrown by any driver and turn it into a {@link DbQueryError}.
63
+ * Drivers put the SQLSTATE in different places: `code` (node-postgres, postgres.js, PGlite),
64
+ * `errno` (Bun.sql) or `sqlState`. Anything else (connection errors, bugs) is returned unchanged.
65
+ */
66
+ const normalizeDbError = (e) => {
67
+ if (e instanceof DbQueryError || !e || typeof e !== "object") return e;
68
+ const x = e;
69
+ for (const key of [
70
+ "sqlState",
71
+ "code",
72
+ "errno"
73
+ ]) {
74
+ const v = x[key];
75
+ 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);
76
+ }
77
+ return e;
78
+ };
79
+ /**
80
+ * SQLSTATE → HTTP status, following the PostgREST 16 table
81
+ * (https://docs.postgrest.org/en/v16/references/errors.html).
82
+ */
83
+ const statusFromSqlState = (state, role, anon) => {
84
+ if (/^PT\d{3}$/.test(state)) return Number(state.slice(2));
85
+ switch (state) {
86
+ case "23503":
87
+ case "23505": return 409;
88
+ case "25006": return 405;
89
+ case "42883":
90
+ case "42P01": return 404;
91
+ case "42P17": return 500;
92
+ case "42501": return role === anon ? 401 : 403;
93
+ case "P0001": return 400;
94
+ case "53400": return 500;
95
+ }
96
+ switch (state.slice(0, 2)) {
97
+ case "08":
98
+ case "53": return 503;
99
+ case "0L":
100
+ case "0P":
101
+ case "28": return 403;
102
+ case "09":
103
+ case "25":
104
+ case "2D":
105
+ case "38":
106
+ case "39":
107
+ case "3B":
108
+ case "40":
109
+ case "54":
110
+ case "55":
111
+ case "57":
112
+ case "58":
113
+ case "F0":
114
+ case "HV":
115
+ case "P0":
116
+ case "XX": return 500;
117
+ default: return 400;
118
+ }
119
+ };
120
+ /**
121
+ * Map a Postgres error to a PostgREST-style API error.
122
+ *
123
+ * Supports PostgREST's custom error conventions:
124
+ * - `RAISE ... USING ERRCODE = 'PTxyz'` → HTTP status xyz
125
+ * - `RAISE SQLSTATE 'PGRST'` with a JSON message/detail
126
+ */
127
+ const dbErrorToApiError = (err, role, anonymousRole) => {
128
+ if (err.sqlState === "PGRST") try {
129
+ const msg = JSON.parse(err.message);
130
+ const det = err.detail ? JSON.parse(err.detail) : {};
131
+ return new ApiError({
132
+ status: det.status ?? 500,
133
+ headers: det.headers ?? {},
134
+ body: {
135
+ code: msg.code ?? "PGRST",
136
+ message: msg.message ?? "",
137
+ details: msg.details ?? null,
138
+ hint: msg.hint ?? null
139
+ }
140
+ });
141
+ } catch {
142
+ return new ApiError({
143
+ status: 500,
144
+ headers: {},
145
+ body: {
146
+ code: "PGRST121",
147
+ message: "Could not parse JSON in the RAISE error",
148
+ details: err.message,
149
+ hint: null
150
+ }
151
+ });
152
+ }
153
+ return new ApiError({
154
+ status: statusFromSqlState(err.sqlState, role, anonymousRole),
155
+ headers: {},
156
+ body: {
157
+ code: err.sqlState,
158
+ message: err.message,
159
+ details: err.detail ?? null,
160
+ hint: err.hint ?? null
161
+ }
162
+ });
163
+ };
164
+
165
+ //#endregion
166
+ //#region src/quote.ts
167
+ /** Quote an SQL identifier: `a"b` → `"a""b"`. */
168
+ const quoteIdent = (name) => {
169
+ if (name.includes("\0")) throw new PgRestError(400, "PGRST100", "Invalid identifier", "contains a NUL byte");
170
+ return `"${name.replace(/"/g, "\"\"")}"`;
171
+ };
172
+ const quoteQualified = (schema, name) => `${quoteIdent(schema)}.${quoteIdent(name)}`;
173
+ /**
174
+ * Escape a string as an SQL literal of *unknown* type, so Postgres infers the
175
+ * type from context (`int_col = '5'` works). Mirrors `pg.escapeLiteral`.
176
+ *
177
+ * We inline literals (like PostgREST does) instead of binding parameters
178
+ * because some drivers (Bun.sql) re-encode string parameters as JSON when the
179
+ * inferred type is json/jsonb.
180
+ */
181
+ const literal = (value) => {
182
+ if (value.includes("\0")) throw new PgRestError(400, "PGRST100", "Invalid value", "contains a NUL byte");
183
+ let out = `'${value.replace(/'/g, "''")}'`;
184
+ if (out.includes("\\")) out = `E${out.replace(/\\/g, "\\\\")}`;
185
+ return out;
186
+ };
187
+ const ID = "[A-Za-z_][A-Za-z0-9_]*";
188
+ 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");
189
+ /** Validate a user-supplied cast target (`::type`) and return it verbatim. */
190
+ const safeType = (type) => {
191
+ const t = type.trim();
192
+ if (t.length > 100 || !TYPE_RE.test(t)) throw new PgRestError(400, "PGRST100", "Invalid type in cast", `"${type}" is not an allowed cast target`);
193
+ return t;
194
+ };
195
+
196
+ //#endregion
197
+ //#region src/schema.ts
198
+ const relKey = (schema, name) => `${schema}.${name}`;
199
+ const kindOf = (k) => k === "v" ? "view" : k === "m" ? "matview" : k === "f" ? "foreign" : k === "p" ? "partitioned" : "table";
200
+ const schemaList = (schemas) => `ARRAY[${schemas.map(literal).join(",")}]::text[]`;
201
+ const asJson = (inner) => `SELECT coalesce(json_agg(r), '[]'::json)::text AS j FROM (${inner}) r`;
202
+ const run = async (db, sql) => {
203
+ const j = (await db.query(asJson(sql)))[0]?.["j"];
204
+ return JSON.parse(typeof j === "string" ? j : "[]");
205
+ };
206
+ /**
207
+ * Foreign keys seen through views: a view that exposes the columns of a foreign key (directly, or
208
+ * through other views) gets the same relationships as the table, towards tables and views alike.
209
+ */
210
+ const viewForeignKeys = (fks, rows) => {
211
+ const viewCols = /* @__PURE__ */ new Map();
212
+ for (const r of rows) {
213
+ const key = relKey(r.vschema, r.vname);
214
+ const m = viewCols.get(key) ?? /* @__PURE__ */ new Map();
215
+ m.set(r.vcol, {
216
+ key: relKey(r.tschema, r.tname),
217
+ col: r.tcol
218
+ });
219
+ viewCols.set(key, m);
220
+ }
221
+ const base = (key, col, depth = 0) => {
222
+ const next = viewCols.get(key)?.get(col);
223
+ return next && depth < 16 ? base(next.key, next.col, depth + 1) : {
224
+ key,
225
+ col
226
+ };
227
+ };
228
+ /** Every view that exposes all of `cols` of table `tableKey`, with the view-side column names. */
229
+ const exposing = (tableKey, cols) => {
230
+ const out$1 = [];
231
+ for (const [vkey, m] of viewCols) {
232
+ const names = [];
233
+ for (const c of cols) {
234
+ let found;
235
+ for (const [vcol] of m) {
236
+ const b = base(vkey, vcol);
237
+ if (b.key === tableKey && b.col === c) {
238
+ found = vcol;
239
+ break;
240
+ }
241
+ }
242
+ if (found === void 0) break;
243
+ names.push(found);
244
+ }
245
+ if (names.length === cols.length) {
246
+ const [schema, ...rest] = vkey.split(".");
247
+ out$1.push({
248
+ schema,
249
+ name: rest.join("."),
250
+ cols: names
251
+ });
252
+ }
253
+ }
254
+ return out$1;
255
+ };
256
+ const out = [];
257
+ for (const fk of fks) {
258
+ const sources = [{
259
+ schema: fk.schema,
260
+ name: fk.table,
261
+ cols: [...fk.cols]
262
+ }, ...exposing(relKey(fk.schema, fk.table), fk.cols)];
263
+ const targets = [{
264
+ schema: fk.fschema,
265
+ name: fk.ftable,
266
+ cols: [...fk.fcols]
267
+ }, ...exposing(relKey(fk.fschema, fk.ftable), fk.fcols)];
268
+ for (const [i, s] of sources.entries()) for (const [j, t] of targets.entries()) {
269
+ if (i === 0 && j === 0) continue;
270
+ out.push({
271
+ name: fk.name,
272
+ schema: s.schema,
273
+ table: s.name,
274
+ cols: s.cols,
275
+ fschema: t.schema,
276
+ ftable: t.name,
277
+ fcols: t.cols
278
+ });
279
+ }
280
+ }
281
+ return out;
282
+ };
283
+ /** Read the catalog and build a fresh {@link SchemaCache}. */
284
+ const introspect = async (db, schemas) => {
285
+ const S = schemaList(schemas);
286
+ const relRows = await run(db, `SELECT n.nspname AS schema, c.relname AS name, c.relkind::text AS kind,
287
+ obj_description(c.oid, 'pg_class') AS comment,
288
+ pg_relation_is_updatable(c.oid, true) AS bits
289
+ FROM pg_class c
290
+ JOIN pg_namespace n ON n.oid = c.relnamespace
291
+ WHERE n.nspname = ANY(${S})
292
+ AND c.relkind IN ('r','v','m','f','p')
293
+ AND NOT c.relispartition
294
+ ORDER BY 1, 2`);
295
+ const colRows = await run(db, `SELECT n.nspname AS schema, c.relname AS "table", a.attname AS name,
296
+ format_type(a.atttypid, a.atttypmod) AS type,
297
+ (SELECT json_agg(e.enumlabel ORDER BY e.enumsortorder) FROM pg_enum e
298
+ WHERE e.enumtypid = CASE WHEN bt.typtype = 'e' THEN bt.oid
299
+ WHEN et.typtype = 'e' THEN et.oid END) AS enum_labels,
300
+ NOT a.attnotnull AS nullable,
301
+ (a.atthasdef OR a.attidentity <> '') AS has_default,
302
+ pg_get_expr(d.adbin, d.adrelid) AS default_expr,
303
+ (a.attgenerated <> '') AS generated,
304
+ a.attidentity::text AS identity,
305
+ col_description(c.oid, a.attnum) AS comment,
306
+ a.attnum AS pos
307
+ FROM pg_attribute a
308
+ JOIN pg_class c ON c.oid = a.attrelid
309
+ JOIN pg_namespace n ON n.oid = c.relnamespace
310
+ LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
311
+ LEFT JOIN pg_type bt ON bt.oid = a.atttypid
312
+ LEFT JOIN pg_type et ON et.oid = bt.typelem
313
+ WHERE n.nspname = ANY(${S})
314
+ AND c.relkind IN ('r','v','m','f','p')
315
+ AND NOT c.relispartition
316
+ AND a.attnum > 0 AND NOT a.attisdropped
317
+ ORDER BY 1, 2, a.attnum`);
318
+ const conRows = await run(db, `SELECT con.conname AS name, con.contype::text AS type,
319
+ ns.nspname AS schema, rel.relname AS "table",
320
+ (SELECT json_agg(a.attname ORDER BY k.ord)
321
+ FROM unnest(con.conkey) WITH ORDINALITY k(attnum, ord)
322
+ JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = k.attnum) AS cols,
323
+ fns.nspname AS fschema, frel.relname AS ftable,
324
+ (SELECT json_agg(a.attname ORDER BY k.ord)
325
+ FROM unnest(con.confkey) WITH ORDINALITY k(attnum, ord)
326
+ JOIN pg_attribute a ON a.attrelid = con.confrelid AND a.attnum = k.attnum) AS fcols
327
+ FROM pg_constraint con
328
+ JOIN pg_class rel ON rel.oid = con.conrelid
329
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
330
+ LEFT JOIN pg_class frel ON frel.oid = con.confrelid
331
+ LEFT JOIN pg_namespace fns ON fns.oid = frel.relnamespace
332
+ WHERE con.contype IN ('p','u','f')
333
+ AND con.conparentid = 0
334
+ AND (ns.nspname = ANY(${S}) OR fns.nspname = ANY(${S}))`);
335
+ const viewRows = await run(db, `SELECT DISTINCT ON (vc.oid, m.x[1]::int)
336
+ vn.nspname AS vschema, vc.relname AS vname, va.attname AS vcol,
337
+ tn.nspname AS tschema, tc.relname AS tname, ta.attname AS tcol
338
+ FROM pg_rewrite rw
339
+ JOIN pg_class vc ON vc.oid = rw.ev_class AND vc.relkind IN ('v','m')
340
+ JOIN pg_namespace vn ON vn.oid = vc.relnamespace
341
+ CROSS JOIN LATERAL regexp_matches(rw.ev_action::text,
342
+ ':resno (\\d+) :resname [^:]* :ressortgroupref \\d+ :resorigtbl (\\d+) :resorigcol (\\d+) :resjunk false', 'g')
343
+ WITH ORDINALITY AS m(x, ord)
344
+ JOIN pg_attribute va ON va.attrelid = vc.oid AND va.attnum = m.x[1]::int AND NOT va.attisdropped
345
+ JOIN pg_class tc ON tc.oid = m.x[2]::oid
346
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
347
+ JOIN pg_attribute ta ON ta.attrelid = tc.oid AND ta.attnum = m.x[3]::int
348
+ WHERE vn.nspname = ANY(${S}) AND rw.rulename = '_RETURN' AND m.x[2]::oid <> 0
349
+ ORDER BY vc.oid, m.x[1]::int, m.ord DESC`);
350
+ const fnRows = await run(db, `SELECT n.nspname AS schema, p.proname AS name,
351
+ p.proretset AS returns_set, p.provolatile::text AS volatility,
352
+ format_type(p.prorettype, NULL) AS return_type,
353
+ (SELECT json_agg(e.enumlabel ORDER BY e.enumsortorder) FROM pg_enum e
354
+ WHERE e.enumtypid = CASE WHEN rt.typtype = 'e' THEN rt.oid
355
+ WHEN ret_el.typtype = 'e' THEN ret_el.oid END) AS return_enum,
356
+ rt.typtype::text AS return_typtype,
357
+ coalesce(p.proargmodes && ARRAY['o','b','t']::"char"[], false) AS has_out,
358
+ p.pronargdefaults AS n_defaults,
359
+ obj_description(p.oid, 'pg_proc') AS comment,
360
+ p.prorows::float8 AS rows,
361
+ CASE WHEN rt.typtype = 'd' AND rt.typname LIKE '%/%'
362
+ THEN json_build_object('name', rt.typname, 'bytea', rt.typbasetype = 'bytea'::regtype) END AS ret_media,
363
+ (SELECT json_build_object('schema', n2.nspname, 'name', c2.relname)
364
+ FROM pg_type t2 JOIN pg_class c2 ON c2.oid = t2.typrelid JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
365
+ WHERE p.pronargs = 1 AND t2.oid = p.proargtypes[0] AND c2.relkind IN ('r','v','m','f','p')) AS arg_rel,
366
+ (SELECT json_build_object('schema', n2.nspname, 'name', c2.relname)
367
+ FROM pg_type t2 JOIN pg_class c2 ON c2.oid = t2.typrelid JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
368
+ WHERE t2.oid = p.prorettype AND c2.relkind IN ('r','v','m','f','p')) AS ret_rel,
369
+ (SELECT coalesce(json_agg(json_build_object(
370
+ 'name', coalesce(p.proargnames[t.i], ''),
371
+ 'type', format_type(t.typ, NULL),
372
+ 'enum', (SELECT json_agg(e.enumlabel ORDER BY e.enumsortorder) FROM pg_enum e
373
+ WHERE e.enumtypid = (SELECT CASE WHEN at.typtype = 'e' THEN at.oid
374
+ WHEN ael.typtype = 'e' THEN ael.oid END
375
+ FROM pg_type at
376
+ LEFT JOIN pg_type ael ON ael.oid = at.typelem
377
+ WHERE at.oid = t.typ)),
378
+ 'mode', coalesce(p.proargmodes[t.i]::text, 'i')) ORDER BY t.i), '[]'::json)
379
+ FROM unnest(coalesce(p.proallargtypes, string_to_array(p.proargtypes::text, ' ')::oid[]))
380
+ WITH ORDINALITY AS t(typ, i)
381
+ WHERE coalesce(p.proargmodes[t.i]::text, 'i') IN ('i','b','v')) AS args
382
+ FROM pg_proc p
383
+ JOIN pg_namespace n ON n.oid = p.pronamespace
384
+ JOIN pg_type rt ON rt.oid = p.prorettype
385
+ LEFT JOIN pg_type ret_el ON ret_el.oid = rt.typelem
386
+ WHERE n.nspname = ANY(${S})
387
+ AND p.prokind = 'f'
388
+ AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
389
+ -- overloads share a name, so the signature breaks the tie: without it the order is whatever
390
+ -- the catalog scan returns, which differs between a database and a copy of it, and the
391
+ -- generated types churn
392
+ ORDER BY 1, 2, pg_get_function_identity_arguments(p.oid)`);
393
+ const mediaHandlers = (await run(db, `SELECT n.nspname AS schema, p.proname AS name,
394
+ (SELECT json_build_object('schema', n2.nspname, 'name', c2.relname)
395
+ FROM pg_type t2 JOIN pg_class c2 ON c2.oid = t2.typrelid JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
396
+ WHERE t2.oid = p.proargtypes[0] AND c2.relkind IN ('r','v','m','f','p')) AS arg_rel,
397
+ rt.typbasetype = 'bytea'::regtype AS bytea
398
+ FROM pg_proc p
399
+ JOIN pg_namespace n ON n.oid = p.pronamespace
400
+ JOIN pg_type rt ON rt.oid = p.prorettype AND rt.typtype = 'd'
401
+ WHERE n.nspname = ANY(${S})
402
+ AND p.prokind = 'a' AND p.pronargs = 1 AND p.proname LIKE '%/%'
403
+ AND (p.proargtypes[0] = 'anyelement'::regtype
404
+ OR EXISTS (SELECT 1 FROM pg_type t3 JOIN pg_class c3 ON c3.oid = t3.typrelid WHERE t3.oid = p.proargtypes[0]))
405
+ ORDER BY 1, 2`)).map((h) => ({
406
+ schema: h.schema,
407
+ mediaType: h.name.toLowerCase(),
408
+ ...h.arg_rel ? { argRelation: h.arg_rel } : {},
409
+ bytea: h.bytea
410
+ }));
411
+ const pk = /* @__PURE__ */ new Map();
412
+ const uniques = /* @__PURE__ */ new Map();
413
+ const foreignKeys = [];
414
+ for (const c of conRows) {
415
+ const key = relKey(c.schema, c.table);
416
+ if (c.type === "p") pk.set(key, c.cols);
417
+ else if (c.type === "u") uniques.set(key, [...uniques.get(key) ?? [], c.cols]);
418
+ else if (c.type === "f" && c.fschema && c.ftable && c.fcols) foreignKeys.push({
419
+ name: c.name,
420
+ schema: c.schema,
421
+ table: c.table,
422
+ cols: c.cols,
423
+ fschema: c.fschema,
424
+ ftable: c.ftable,
425
+ fcols: c.fcols
426
+ });
427
+ }
428
+ foreignKeys.push(...viewForeignKeys(foreignKeys, viewRows));
429
+ const viewSource = /* @__PURE__ */ new Map();
430
+ {
431
+ const direct = /* @__PURE__ */ new Map();
432
+ for (const r of viewRows) direct.set(`${relKey(r.vschema, r.vname)}.${r.vcol}`, {
433
+ key: relKey(r.tschema, r.tname),
434
+ col: r.tcol
435
+ });
436
+ for (const [from, first] of direct) {
437
+ let at = first;
438
+ for (let depth = 0; depth < 16; depth++) {
439
+ const next = direct.get(`${at.key}.${at.col}`);
440
+ if (!next) break;
441
+ at = next;
442
+ }
443
+ const [schema, ...rest] = at.key.split(".");
444
+ viewSource.set(from, {
445
+ schema,
446
+ table: rest.join("."),
447
+ column: at.col
448
+ });
449
+ }
450
+ }
451
+ const colsByRel = /* @__PURE__ */ new Map();
452
+ for (const c of colRows) {
453
+ const key = relKey(c.schema, c.table);
454
+ const list = colsByRel.get(key) ?? [];
455
+ const source = viewSource.get(`${key}.${c.name}`);
456
+ list.push({
457
+ name: c.name,
458
+ type: c.type,
459
+ ...c.enum_labels ? { enumLabels: c.enum_labels } : {},
460
+ ...source ? { source } : {},
461
+ nullable: c.nullable,
462
+ hasDefault: c.has_default,
463
+ defaultExpr: c.default_expr,
464
+ generated: c.generated,
465
+ ...c.identity === "a" ? { identity: "always" } : c.identity === "d" ? { identity: "by-default" } : {},
466
+ comment: c.comment,
467
+ position: c.pos
468
+ });
469
+ colsByRel.set(key, list);
470
+ }
471
+ const relations = /* @__PURE__ */ new Map();
472
+ for (const r of relRows) {
473
+ const key = relKey(r.schema, r.name);
474
+ relations.set(key, {
475
+ schema: r.schema,
476
+ name: r.name,
477
+ kind: kindOf(r.kind),
478
+ columns: colsByRel.get(key) ?? [],
479
+ pk: pk.get(key) ?? [],
480
+ uniques: uniques.get(key) ?? [],
481
+ insertable: (r.bits & 8) === 8,
482
+ updatable: (r.bits & 4) === 4,
483
+ deletable: (r.bits & 16) === 16,
484
+ comment: r.comment
485
+ });
486
+ }
487
+ const functions = /* @__PURE__ */ new Map();
488
+ for (const f of fnRows) {
489
+ const nDefaults = f.n_defaults;
490
+ const args = f.args.map((a, i) => ({
491
+ name: a.name,
492
+ type: a.type,
493
+ hasDefault: i >= f.args.length - nDefaults,
494
+ variadic: a.mode === "v",
495
+ ...a.enum ? { enumLabels: a.enum } : {}
496
+ }));
497
+ const def = {
498
+ schema: f.schema,
499
+ name: f.name,
500
+ args,
501
+ returnType: f.return_type,
502
+ ...f.return_enum ? { returnEnumLabels: f.return_enum } : {},
503
+ returnsSet: f.returns_set,
504
+ returnsComposite: f.has_out || f.return_typtype === "c" || f.return_type === "record",
505
+ returnsVoid: f.return_type === "void",
506
+ volatility: f.volatility === "i" ? "immutable" : f.volatility === "s" ? "stable" : "volatile",
507
+ comment: f.comment,
508
+ rows: f.rows,
509
+ ...f.arg_rel ? { argRelation: f.arg_rel } : {},
510
+ ...f.ret_rel ? { returnRelation: f.ret_rel } : {},
511
+ ...f.ret_media && !f.returns_set ? { returnMediaType: f.ret_media } : {}
512
+ };
513
+ const key = relKey(f.schema, f.name);
514
+ functions.set(key, [...functions.get(key) ?? [], def]);
515
+ }
516
+ return {
517
+ schemas,
518
+ relations,
519
+ foreignKeys,
520
+ functions,
521
+ mediaHandlers
522
+ };
523
+ };
524
+ const sameCols = (a, b) => a.length === b.length && a.every((c) => b.includes(c));
525
+ const isUnique = (rel, cols) => rel.pk.length > 0 && sameCols(rel.pk, cols) || rel.uniques.some((u) => sameCols(u, cols));
526
+ const describe = (r) => {
527
+ const j = r.junction;
528
+ if (j) return `'${j.relation.name}' using '${j.fkFrom.name}' and '${j.fkTo.name}' (many-to-many)`;
529
+ if (r.computed) return `'${r.computed.name}' (computed, ${r.kind === "m2o" ? "many-to-one" : "one-to-many"})`;
530
+ 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(",")})`;
531
+ };
532
+ /** Find the relationships between `from` and the table/view named `target`. */
533
+ const findRelationships = (cache, from, target, hint) => {
534
+ const out = [];
535
+ const rel = (schema, name) => cache.relations.get(relKey(schema, name));
536
+ for (const fk of cache.foreignKeys) {
537
+ const selfReference = fk.schema === fk.fschema && fk.table === fk.ftable;
538
+ if (fk.schema === from.schema && fk.table === from.name && fk.ftable === target && !selfReference) {
539
+ const to = rel(fk.fschema, fk.ftable);
540
+ if (to) out.push({
541
+ kind: "m2o",
542
+ from,
543
+ to,
544
+ fromCols: fk.cols,
545
+ toCols: fk.fcols,
546
+ fk
547
+ });
548
+ }
549
+ if (fk.schema === from.schema && fk.table === from.name && fk.cols.length === 1 && fk.cols[0] === target) {
550
+ const to = rel(fk.fschema, fk.ftable);
551
+ if (to) out.push({
552
+ kind: "m2o",
553
+ from,
554
+ to,
555
+ fromCols: fk.cols,
556
+ toCols: fk.fcols,
557
+ fk
558
+ });
559
+ }
560
+ if (fk.fschema === from.schema && fk.ftable === from.name && fk.table === target) {
561
+ const to = rel(fk.schema, fk.table);
562
+ if (to) out.push({
563
+ kind: isUnique(to, fk.cols) ? "o2o" : "o2m",
564
+ from,
565
+ to,
566
+ fromCols: fk.fcols,
567
+ toCols: fk.cols,
568
+ fk
569
+ });
570
+ }
571
+ }
572
+ for (const f of cache.functions.get(relKey(from.schema, target)) ?? []) {
573
+ if (f.argRelation?.schema !== from.schema || f.argRelation.name !== from.name || !f.returnRelation) continue;
574
+ const to = rel(f.returnRelation.schema, f.returnRelation.name);
575
+ if (!to) continue;
576
+ const fk = {
577
+ name: f.name,
578
+ schema: from.schema,
579
+ table: from.name,
580
+ cols: [],
581
+ fschema: to.schema,
582
+ ftable: to.name,
583
+ fcols: []
584
+ };
585
+ out.push({
586
+ kind: !f.returnsSet || f.rows === 1 ? "m2o" : "o2m",
587
+ from,
588
+ to,
589
+ fromCols: [],
590
+ toCols: [],
591
+ fk,
592
+ computed: f
593
+ });
594
+ }
595
+ for (const fk1 of cache.foreignKeys) {
596
+ if (fk1.fschema !== from.schema || fk1.ftable !== from.name) continue;
597
+ const junction = rel(fk1.schema, fk1.table);
598
+ if (!junction) continue;
599
+ for (const fk2 of cache.foreignKeys) {
600
+ if (fk2 === fk1) continue;
601
+ if (fk2.schema !== fk1.schema || fk2.table !== fk1.table) continue;
602
+ if (fk2.ftable !== target) continue;
603
+ const to = rel(fk2.fschema, fk2.ftable);
604
+ if (!to) continue;
605
+ out.push({
606
+ kind: "m2m",
607
+ from,
608
+ to,
609
+ fromCols: fk1.fcols,
610
+ toCols: fk2.fcols,
611
+ fk: fk1,
612
+ junction: {
613
+ relation: junction,
614
+ fkFrom: fk1,
615
+ fkTo: fk2
616
+ }
617
+ });
618
+ }
619
+ }
620
+ if (!hint) return out;
621
+ return out.filter((r) => {
622
+ if (r.junction) return r.junction.relation.name === hint || r.junction.fkFrom.name === hint || r.junction.fkTo.name === hint;
623
+ return r.fk.name === hint || r.fk.cols.includes(hint) || r.fk.fcols.includes(hint);
624
+ });
625
+ };
626
+ /**
627
+ * Resolve exactly one relationship or throw PostgREST-style PGRST200/PGRST201.
628
+ * `target` may also be a table that is not related at all (→ PGRST200).
629
+ */
630
+ const resolveRelationship = (cache, from, target, hint) => {
631
+ const found = findRelationships(cache, from, target, hint);
632
+ if (found.length === 1) return found[0];
633
+ 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);
634
+ 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.`);
635
+ };
636
+ /** Look up a relation by (optional) schema and name, or throw PGRST205. */
637
+ const getRelation = (cache, schema, name) => {
638
+ const rel = cache.relations.get(relKey(schema, name));
639
+ if (!rel) throw new PgRestError(404, "PGRST205", `Could not find the table '${schema}.${name}' in the schema cache`, null, suggest(cache, schema, name));
640
+ return rel;
641
+ };
642
+ const suggest = (cache, schema, name) => {
643
+ let best;
644
+ for (const r of cache.relations.values()) {
645
+ if (r.schema !== schema) continue;
646
+ const d = levenshtein(name.toLowerCase(), r.name.toLowerCase());
647
+ if (d <= Math.max(1, Math.floor(name.length / 4)) && (!best || d < best.d)) best = {
648
+ name: r.name,
649
+ d
650
+ };
651
+ }
652
+ return best ? `Perhaps you meant the table '${schema}.${best.name}'` : null;
653
+ };
654
+ const levenshtein = (a, b) => {
655
+ const dp = Array.from({ length: b.length + 1 }, (_, i) => i);
656
+ for (let i = 1; i <= a.length; i++) {
657
+ let prev = dp[0];
658
+ dp[0] = i;
659
+ for (let j = 1; j <= b.length; j++) {
660
+ const tmp = dp[j];
661
+ dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
662
+ prev = tmp;
663
+ }
664
+ }
665
+ return dp[b.length];
666
+ };
667
+
668
+ //#endregion
669
+ export { relKey as a, quoteIdent as c, ApiError as d, DbQueryError as f, normalizeDbError as h, levenshtein as i, quoteQualified as l, dbErrorToApiError as m, getRelation as n, resolveRelationship as o, PgRestError as p, introspect as r, literal as s, findRelationships as t, safeType as u };