@adrdp/pg-rest 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -134
- package/dist/adapters/bun.d.ts +1 -1
- package/dist/adapters/node-postgres.d.ts +1 -1
- package/dist/adapters/pglite.d.ts +1 -1
- package/dist/adapters/postgres-js.d.ts +1 -1
- package/dist/index.d.ts +642 -130
- package/dist/index.js +380 -648
- package/dist/schema-BhF_TQdX.js +669 -0
- package/dist/types-DaNxiC7H.d.ts +146 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +202 -0
- package/package.json +7 -1
- /package/dist/{adapter-SqPs6SuJ.d.ts → adapter-C2udR5IT.d.ts} +0 -0
|
@@ -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 };
|