@pgsql/transform 18.5.0 → 18.7.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/esm/extension-router.js +236 -0
- package/esm/extension-transform.js +202 -0
- package/esm/facts.js +54 -0
- package/esm/index.js +4 -0
- package/esm/role-router.js +62 -0
- package/esm/role-transform.js +129 -0
- package/esm/router.js +69 -5
- package/esm/transform.js +32 -9
- package/extension-router.d.ts +161 -0
- package/extension-router.js +240 -0
- package/extension-transform.d.ts +48 -0
- package/extension-transform.js +207 -0
- package/facts.d.ts +37 -1
- package/facts.js +54 -0
- package/index.d.ts +10 -2
- package/index.js +14 -1
- package/package.json +2 -2
- package/role-router.d.ts +42 -0
- package/role-router.js +66 -0
- package/role-transform.d.ts +50 -0
- package/role-transform.js +134 -0
- package/router.d.ts +63 -6
- package/router.js +69 -5
- package/transform.js +32 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgsql/transform",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.7.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "AST-based SQL transformation, qualification, classification and closure analysis for PostgreSQL",
|
|
6
6
|
"main": "index.js",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"classify",
|
|
50
50
|
"plpgsql"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "057e427849438bfea946d6d85557603ba7fea134"
|
|
53
53
|
}
|
package/role-router.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-name routing for the core transform.
|
|
3
|
+
*
|
|
4
|
+
* Two databases can express the *same* access model with *different* role
|
|
5
|
+
* names — one calls the unauthenticated role `anonymous`, another `anon`; one
|
|
6
|
+
* calls the privileged role `administrator`, another something else. Portable
|
|
7
|
+
* SQL must be able to translate role identifiers between these conventions.
|
|
8
|
+
*
|
|
9
|
+
* Scope, deliberately narrow: a {@link RoleRouter} only renames role
|
|
10
|
+
* *identifiers* in the AST positions where a role name legitimately appears
|
|
11
|
+
* (grants, ownership, policies, default privileges, role membership, role
|
|
12
|
+
* settings). It does **not** alter role *attributes* (`BYPASSRLS`, `LOGIN`,
|
|
13
|
+
* ...): two roles that share a name across conventions do not necessarily
|
|
14
|
+
* share privilege scope, and inventing attribute changes would silently
|
|
15
|
+
* misrepresent the security model. Privilege-semantics reconciliation is a
|
|
16
|
+
* downstream, policy-level concern; the router restricts itself to the
|
|
17
|
+
* deterministic, reversible part — the name.
|
|
18
|
+
*
|
|
19
|
+
* Renaming is a pure identifier substitution, so a router is trivially
|
|
20
|
+
* invertible ({@link RoleRouter.invert}) for bidirectional translation.
|
|
21
|
+
*/
|
|
22
|
+
/** Role-rename specification: source role name -> target role name. */
|
|
23
|
+
export type RoleRouteSpec = Record<string, string>;
|
|
24
|
+
/** Resolves a target role name for a source role name. */
|
|
25
|
+
export declare class RoleRouter {
|
|
26
|
+
private readonly map;
|
|
27
|
+
constructor(routes?: RoleRouteSpec | Map<string, string>);
|
|
28
|
+
/** Coerce a spec, map, or existing router into a router. */
|
|
29
|
+
static from(source: RoleRouter | RoleRouteSpec | Map<string, string>): RoleRouter;
|
|
30
|
+
/** Number of configured renames. */
|
|
31
|
+
get size(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the target name for `role`, or `undefined` when it is not routed
|
|
34
|
+
* or already at its destination.
|
|
35
|
+
*/
|
|
36
|
+
resolve(role: string | undefined | null): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The inverse router (target -> source), for translating in the opposite
|
|
39
|
+
* direction. Throws if the mapping is not one-to-one.
|
|
40
|
+
*/
|
|
41
|
+
invert(): RoleRouter;
|
|
42
|
+
}
|
package/role-router.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Role-name routing for the core transform.
|
|
4
|
+
*
|
|
5
|
+
* Two databases can express the *same* access model with *different* role
|
|
6
|
+
* names — one calls the unauthenticated role `anonymous`, another `anon`; one
|
|
7
|
+
* calls the privileged role `administrator`, another something else. Portable
|
|
8
|
+
* SQL must be able to translate role identifiers between these conventions.
|
|
9
|
+
*
|
|
10
|
+
* Scope, deliberately narrow: a {@link RoleRouter} only renames role
|
|
11
|
+
* *identifiers* in the AST positions where a role name legitimately appears
|
|
12
|
+
* (grants, ownership, policies, default privileges, role membership, role
|
|
13
|
+
* settings). It does **not** alter role *attributes* (`BYPASSRLS`, `LOGIN`,
|
|
14
|
+
* ...): two roles that share a name across conventions do not necessarily
|
|
15
|
+
* share privilege scope, and inventing attribute changes would silently
|
|
16
|
+
* misrepresent the security model. Privilege-semantics reconciliation is a
|
|
17
|
+
* downstream, policy-level concern; the router restricts itself to the
|
|
18
|
+
* deterministic, reversible part — the name.
|
|
19
|
+
*
|
|
20
|
+
* Renaming is a pure identifier substitution, so a router is trivially
|
|
21
|
+
* invertible ({@link RoleRouter.invert}) for bidirectional translation.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.RoleRouter = void 0;
|
|
25
|
+
/** Resolves a target role name for a source role name. */
|
|
26
|
+
class RoleRouter {
|
|
27
|
+
map;
|
|
28
|
+
constructor(routes = {}) {
|
|
29
|
+
this.map = routes instanceof Map ? new Map(routes) : new Map(Object.entries(routes));
|
|
30
|
+
}
|
|
31
|
+
/** Coerce a spec, map, or existing router into a router. */
|
|
32
|
+
static from(source) {
|
|
33
|
+
return source instanceof RoleRouter ? source : new RoleRouter(source);
|
|
34
|
+
}
|
|
35
|
+
/** Number of configured renames. */
|
|
36
|
+
get size() {
|
|
37
|
+
return this.map.size;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the target name for `role`, or `undefined` when it is not routed
|
|
41
|
+
* or already at its destination.
|
|
42
|
+
*/
|
|
43
|
+
resolve(role) {
|
|
44
|
+
if (!role)
|
|
45
|
+
return undefined;
|
|
46
|
+
const target = this.map.get(role);
|
|
47
|
+
if (target === undefined || target === role)
|
|
48
|
+
return undefined;
|
|
49
|
+
return target;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The inverse router (target -> source), for translating in the opposite
|
|
53
|
+
* direction. Throws if the mapping is not one-to-one.
|
|
54
|
+
*/
|
|
55
|
+
invert() {
|
|
56
|
+
const inverted = new Map();
|
|
57
|
+
for (const [from, to] of this.map) {
|
|
58
|
+
if (inverted.has(to)) {
|
|
59
|
+
throw new Error(`RoleRouter.invert: mapping is not one-to-one (two sources map to "${to}")`);
|
|
60
|
+
}
|
|
61
|
+
inverted.set(to, from);
|
|
62
|
+
}
|
|
63
|
+
return new RoleRouter(inverted);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.RoleRouter = RoleRouter;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST transform that applies a {@link RoleRouter}.
|
|
3
|
+
*
|
|
4
|
+
* Role identifiers surface in many statement forms. Most flow through a single
|
|
5
|
+
* `RoleSpec` node (grantees, ownership `... OWNER TO`, `CREATE POLICY ... TO`,
|
|
6
|
+
* `ALTER DEFAULT PRIVILEGES [FOR ROLE ...] ... TO`, `ALTER ROLE`, `DROP ROLE`,
|
|
7
|
+
* role-membership grantees, `GRANTED BY`, `REASSIGN OWNED BY`), which the
|
|
8
|
+
* traversal reaches automatically. Four positions carry a role name outside a
|
|
9
|
+
* `RoleSpec` and are handled explicitly:
|
|
10
|
+
*
|
|
11
|
+
* - `CREATE ROLE <name>` — a bare string on `CreateRoleStmt.role`.
|
|
12
|
+
* - `GRANT <role> TO ...` — the *granted* roles are `AccessPriv.priv_name`
|
|
13
|
+
* strings on `GrantRoleStmt.granted_roles` (not `RoleSpec`s).
|
|
14
|
+
* - `SET ROLE` / `SET SESSION AUTHORIZATION` — the role is a string constant
|
|
15
|
+
* in `VariableSetStmt.args`.
|
|
16
|
+
* - `ALTER ROLE <old> RENAME TO <new>` — `RenameStmt` with string
|
|
17
|
+
* `subname`/`newname`.
|
|
18
|
+
*
|
|
19
|
+
* Reserved role specifications (`PUBLIC`, `CURRENT_USER`, `SESSION_USER`, ...)
|
|
20
|
+
* carry no `rolename` and a non-`CSTRING` `roletype`, so they are never
|
|
21
|
+
* rewritten.
|
|
22
|
+
*/
|
|
23
|
+
import type { RoleRouteSpec } from './role-router';
|
|
24
|
+
import { RoleRouter } from './role-router';
|
|
25
|
+
/** What a role transform changed: role name -> number of positions rewritten. */
|
|
26
|
+
export interface RoleTransformResult {
|
|
27
|
+
rolesRenamed: Map<string, number>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createRoleResult(): RoleTransformResult;
|
|
30
|
+
/**
|
|
31
|
+
* Create a SQL AST visitor that renames role identifiers against a router.
|
|
32
|
+
* Composable with the walkers used by the core transform.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createRoleVisitor(router: RoleRouter, result: RoleTransformResult): {
|
|
35
|
+
RoleSpec: (path: any) => void;
|
|
36
|
+
CreateRoleStmt: (path: any) => void;
|
|
37
|
+
GrantRoleStmt: (path: any) => void;
|
|
38
|
+
VariableSetStmt: (path: any) => void;
|
|
39
|
+
RenameStmt: (path: any) => void;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Apply role renaming to a SQL string: parse -> walk -> deparse. Role names in
|
|
43
|
+
* PL/pgSQL / `LANGUAGE sql` bodies are string literals (e.g. inside dynamic
|
|
44
|
+
* `EXECUTE`) and are intentionally not rewritten — see the module docs on why
|
|
45
|
+
* arbitrary string contents are out of scope.
|
|
46
|
+
*/
|
|
47
|
+
export declare function transformRoles(sql: string, router: RoleRouter | RoleRouteSpec | Map<string, string>): {
|
|
48
|
+
sql: string;
|
|
49
|
+
result: RoleTransformResult;
|
|
50
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AST transform that applies a {@link RoleRouter}.
|
|
4
|
+
*
|
|
5
|
+
* Role identifiers surface in many statement forms. Most flow through a single
|
|
6
|
+
* `RoleSpec` node (grantees, ownership `... OWNER TO`, `CREATE POLICY ... TO`,
|
|
7
|
+
* `ALTER DEFAULT PRIVILEGES [FOR ROLE ...] ... TO`, `ALTER ROLE`, `DROP ROLE`,
|
|
8
|
+
* role-membership grantees, `GRANTED BY`, `REASSIGN OWNED BY`), which the
|
|
9
|
+
* traversal reaches automatically. Four positions carry a role name outside a
|
|
10
|
+
* `RoleSpec` and are handled explicitly:
|
|
11
|
+
*
|
|
12
|
+
* - `CREATE ROLE <name>` — a bare string on `CreateRoleStmt.role`.
|
|
13
|
+
* - `GRANT <role> TO ...` — the *granted* roles are `AccessPriv.priv_name`
|
|
14
|
+
* strings on `GrantRoleStmt.granted_roles` (not `RoleSpec`s).
|
|
15
|
+
* - `SET ROLE` / `SET SESSION AUTHORIZATION` — the role is a string constant
|
|
16
|
+
* in `VariableSetStmt.args`.
|
|
17
|
+
* - `ALTER ROLE <old> RENAME TO <new>` — `RenameStmt` with string
|
|
18
|
+
* `subname`/`newname`.
|
|
19
|
+
*
|
|
20
|
+
* Reserved role specifications (`PUBLIC`, `CURRENT_USER`, `SESSION_USER`, ...)
|
|
21
|
+
* carry no `rolename` and a non-`CSTRING` `roletype`, so they are never
|
|
22
|
+
* rewritten.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.createRoleResult = createRoleResult;
|
|
26
|
+
exports.createRoleVisitor = createRoleVisitor;
|
|
27
|
+
exports.transformRoles = transformRoles;
|
|
28
|
+
const traverse_1 = require("@pgsql/traverse");
|
|
29
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
30
|
+
const role_router_1 = require("./role-router");
|
|
31
|
+
function createRoleResult() {
|
|
32
|
+
return { rolesRenamed: new Map() };
|
|
33
|
+
}
|
|
34
|
+
const ROLE_SET_PARAMS = new Set(['role', 'session_authorization']);
|
|
35
|
+
/**
|
|
36
|
+
* Create a SQL AST visitor that renames role identifiers against a router.
|
|
37
|
+
* Composable with the walkers used by the core transform.
|
|
38
|
+
*/
|
|
39
|
+
function createRoleVisitor(router, result) {
|
|
40
|
+
const record = (name) => {
|
|
41
|
+
result.rolesRenamed.set(name, (result.rolesRenamed.get(name) ?? 0) + 1);
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
RoleSpec: (path) => {
|
|
45
|
+
const node = path.node;
|
|
46
|
+
// Only named roles (ROLESPEC_CSTRING) carry a rolename; PUBLIC /
|
|
47
|
+
// CURRENT_USER / SESSION_USER are encoded by roletype and left alone.
|
|
48
|
+
if (node.roletype !== 'ROLESPEC_CSTRING')
|
|
49
|
+
return;
|
|
50
|
+
const target = router.resolve(node.rolename);
|
|
51
|
+
if (target === undefined)
|
|
52
|
+
return;
|
|
53
|
+
node.rolename = target;
|
|
54
|
+
record(node.rolename);
|
|
55
|
+
},
|
|
56
|
+
CreateRoleStmt: (path) => {
|
|
57
|
+
const node = path.node;
|
|
58
|
+
const target = router.resolve(node.role);
|
|
59
|
+
if (target === undefined)
|
|
60
|
+
return;
|
|
61
|
+
node.role = target;
|
|
62
|
+
record(target);
|
|
63
|
+
},
|
|
64
|
+
GrantRoleStmt: (path) => {
|
|
65
|
+
// granted_roles are role names carried as AccessPriv.priv_name (the
|
|
66
|
+
// grantee_roles RoleSpecs are handled by the RoleSpec visitor).
|
|
67
|
+
const granted = path.node.granted_roles;
|
|
68
|
+
if (!Array.isArray(granted))
|
|
69
|
+
return;
|
|
70
|
+
for (const g of granted) {
|
|
71
|
+
const priv = g?.AccessPriv;
|
|
72
|
+
if (!priv || typeof priv.priv_name !== 'string')
|
|
73
|
+
continue;
|
|
74
|
+
const target = router.resolve(priv.priv_name);
|
|
75
|
+
if (target === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
priv.priv_name = target;
|
|
78
|
+
record(target);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
VariableSetStmt: (path) => {
|
|
82
|
+
const node = path.node;
|
|
83
|
+
if (!ROLE_SET_PARAMS.has(node.name) || !Array.isArray(node.args))
|
|
84
|
+
return;
|
|
85
|
+
for (const arg of node.args) {
|
|
86
|
+
const sval = arg?.A_Const?.sval?.sval ?? arg?.String?.sval;
|
|
87
|
+
if (typeof sval !== 'string')
|
|
88
|
+
continue;
|
|
89
|
+
const target = router.resolve(sval);
|
|
90
|
+
if (target === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
if (arg.A_Const?.sval)
|
|
93
|
+
arg.A_Const.sval.sval = target;
|
|
94
|
+
else if (arg.String)
|
|
95
|
+
arg.String.sval = target;
|
|
96
|
+
record(target);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
RenameStmt: (path) => {
|
|
100
|
+
const node = path.node;
|
|
101
|
+
if (node.renameType !== 'OBJECT_ROLE')
|
|
102
|
+
return;
|
|
103
|
+
const subTarget = router.resolve(node.subname);
|
|
104
|
+
if (subTarget !== undefined) {
|
|
105
|
+
node.subname = subTarget;
|
|
106
|
+
record(subTarget);
|
|
107
|
+
}
|
|
108
|
+
const newTarget = router.resolve(node.newname);
|
|
109
|
+
if (newTarget !== undefined) {
|
|
110
|
+
node.newname = newTarget;
|
|
111
|
+
record(newTarget);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Apply role renaming to a SQL string: parse -> walk -> deparse. Role names in
|
|
118
|
+
* PL/pgSQL / `LANGUAGE sql` bodies are string literals (e.g. inside dynamic
|
|
119
|
+
* `EXECUTE`) and are intentionally not rewritten — see the module docs on why
|
|
120
|
+
* arbitrary string contents are out of scope.
|
|
121
|
+
*/
|
|
122
|
+
function transformRoles(sql, router) {
|
|
123
|
+
const resolved = role_router_1.RoleRouter.from(router);
|
|
124
|
+
const result = createRoleResult();
|
|
125
|
+
const out = (0, plpgsql_parser_1.transformSync)(sql, (ctx) => {
|
|
126
|
+
const stmts = ctx.sql?.stmts ?? [];
|
|
127
|
+
const visitor = createRoleVisitor(resolved, result);
|
|
128
|
+
for (const stmt of stmts) {
|
|
129
|
+
if (stmt?.stmt)
|
|
130
|
+
(0, traverse_1.walk)(stmt.stmt, visitor);
|
|
131
|
+
}
|
|
132
|
+
}, { hydrate: true, pretty: true });
|
|
133
|
+
return { sql: out, result };
|
|
134
|
+
}
|
package/router.d.ts
CHANGED
|
@@ -20,6 +20,19 @@
|
|
|
20
20
|
* `types` — matching `pg_class` / `pg_proc` / `pg_type`), mirroring the routing
|
|
21
21
|
* model already used by {@link qualifyUnqualified}. Resolution is
|
|
22
22
|
* object-route-first, then the schema-level default, then "leave unchanged".
|
|
23
|
+
*
|
|
24
|
+
* An object route may also *rebind* — change the object's **name**, not just
|
|
25
|
+
* the schema it lives in. Routing preserves identity (the same function, a new
|
|
26
|
+
* address); rebinding repoints a reference at a *different* object, which is
|
|
27
|
+
* what lets one implementation of a contract be substituted for another:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* { auth: { functions: { uid: { schema: null, name: 'current_user_id' } } } }
|
|
31
|
+
* // auth.uid() -> current_user_id()
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* A `null` target schema de-qualifies the reference (relying on `search_path`),
|
|
35
|
+
* matching the convention used by the extension router.
|
|
23
36
|
*/
|
|
24
37
|
/** PostgreSQL object namespaces relevant to schema routing. */
|
|
25
38
|
export type ObjectNamespace = 'relation' | 'function' | 'type';
|
|
@@ -30,6 +43,24 @@ export type ObjectNamespace = 'relation' | 'function' | 'type';
|
|
|
30
43
|
* which case only the schema-level default applies.
|
|
31
44
|
*/
|
|
32
45
|
export type RouteNamespace = ObjectNamespace | 'schema' | 'unknown';
|
|
46
|
+
/**
|
|
47
|
+
* Where a specific object should be reached instead. Either field may be
|
|
48
|
+
* omitted: omitting `schema` keeps the schema-level default (or the current
|
|
49
|
+
* schema when the route has none), and omitting `name` keeps the object's own
|
|
50
|
+
* name — so `{ name }` alone is a pure rebind and `{ schema }` alone is
|
|
51
|
+
* equivalent to the shorthand string form.
|
|
52
|
+
*/
|
|
53
|
+
export interface ObjectRoute {
|
|
54
|
+
/** Target schema, or `null` to make the reference unqualified. */
|
|
55
|
+
schema?: string | null;
|
|
56
|
+
/** Target object name — rebinds the reference to a different object. */
|
|
57
|
+
name?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* An object route target. The shorthand `string` form is the target schema,
|
|
61
|
+
* identical to `{ schema: target }`.
|
|
62
|
+
*/
|
|
63
|
+
export type ObjectRouteTarget = string | ObjectRoute;
|
|
33
64
|
/** Per-source-schema routing: a schema-level default plus per-object routes. */
|
|
34
65
|
export interface SchemaRoute {
|
|
35
66
|
/**
|
|
@@ -38,12 +69,12 @@ export interface SchemaRoute {
|
|
|
38
69
|
* and leave the rest (and the schema itself) untouched.
|
|
39
70
|
*/
|
|
40
71
|
schema?: string;
|
|
41
|
-
/** Relation name (table/view/sequence/matview) → target schema. */
|
|
42
|
-
relations?: Record<string,
|
|
43
|
-
/** Function/procedure/aggregate name → target schema. */
|
|
44
|
-
functions?: Record<string,
|
|
45
|
-
/** Type/domain name → target schema. */
|
|
46
|
-
types?: Record<string,
|
|
72
|
+
/** Relation name (table/view/sequence/matview) → target schema or rebind. */
|
|
73
|
+
relations?: Record<string, ObjectRouteTarget>;
|
|
74
|
+
/** Function/procedure/aggregate name → target schema or rebind. */
|
|
75
|
+
functions?: Record<string, ObjectRouteTarget>;
|
|
76
|
+
/** Type/domain name → target schema or rebind. */
|
|
77
|
+
types?: Record<string, ObjectRouteTarget>;
|
|
47
78
|
}
|
|
48
79
|
/** The full routing specification: one {@link SchemaRoute} per source schema. */
|
|
49
80
|
export type RouteSpec = Record<string, SchemaRoute>;
|
|
@@ -68,6 +99,22 @@ export declare class SchemaRouter {
|
|
|
68
99
|
* bodies; whole-schema routes are handled by the cheaper string passes.
|
|
69
100
|
*/
|
|
70
101
|
hasObjectRoutes(): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* True when any object route changes a name or de-qualifies (rather than
|
|
104
|
+
* only moving between schemas). Such rewrites cannot be expressed by the
|
|
105
|
+
* string-level passes at all, so callers use this to require the AST path.
|
|
106
|
+
*/
|
|
107
|
+
hasNameRebinds(): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Every object route that rebinds a name or de-qualifies, keyed by source
|
|
110
|
+
* schema and namespace. Callers use this to report or verify substitutions.
|
|
111
|
+
*/
|
|
112
|
+
nameRebinds(): Array<{
|
|
113
|
+
schema: string;
|
|
114
|
+
ns: ObjectNamespace;
|
|
115
|
+
from: string;
|
|
116
|
+
to: ObjectRoute;
|
|
117
|
+
}>;
|
|
71
118
|
/** Every source schema this router may touch. */
|
|
72
119
|
sourceSchemas(): string[];
|
|
73
120
|
/**
|
|
@@ -77,6 +124,16 @@ export declare class SchemaRouter {
|
|
|
77
124
|
* to any object without a specific route.
|
|
78
125
|
*/
|
|
79
126
|
resolve(sourceSchema: string | undefined | null, name?: string, ns?: RouteNamespace): string | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the full target for `(sourceSchema, name)` in namespace `ns` — both
|
|
129
|
+
* the schema the reference should live in and, when the route rebinds, the
|
|
130
|
+
* name it should be reached by. Returns `undefined` to leave it unchanged.
|
|
131
|
+
*
|
|
132
|
+
* In the result, `schema` is `null` when the reference should become
|
|
133
|
+
* unqualified and `undefined` when only the name changes; `name` is
|
|
134
|
+
* `undefined` when only the schema changes.
|
|
135
|
+
*/
|
|
136
|
+
resolveObject(sourceSchema: string | undefined | null, name?: string, ns?: RouteNamespace): ObjectRoute | undefined;
|
|
80
137
|
/**
|
|
81
138
|
* Source schemas whose *every* object is guaranteed to move — i.e. those
|
|
82
139
|
* with a schema-level default. After a transform, none of these should
|
package/router.js
CHANGED
|
@@ -21,6 +21,19 @@
|
|
|
21
21
|
* `types` — matching `pg_class` / `pg_proc` / `pg_type`), mirroring the routing
|
|
22
22
|
* model already used by {@link qualifyUnqualified}. Resolution is
|
|
23
23
|
* object-route-first, then the schema-level default, then "leave unchanged".
|
|
24
|
+
*
|
|
25
|
+
* An object route may also *rebind* — change the object's **name**, not just
|
|
26
|
+
* the schema it lives in. Routing preserves identity (the same function, a new
|
|
27
|
+
* address); rebinding repoints a reference at a *different* object, which is
|
|
28
|
+
* what lets one implementation of a contract be substituted for another:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* { auth: { functions: { uid: { schema: null, name: 'current_user_id' } } } }
|
|
32
|
+
* // auth.uid() -> current_user_id()
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* A `null` target schema de-qualifies the reference (relying on `search_path`),
|
|
36
|
+
* matching the convention used by the extension router.
|
|
24
37
|
*/
|
|
25
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
39
|
exports.SchemaRouter = void 0;
|
|
@@ -74,6 +87,36 @@ class SchemaRouter {
|
|
|
74
87
|
}
|
|
75
88
|
return false;
|
|
76
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* True when any object route changes a name or de-qualifies (rather than
|
|
92
|
+
* only moving between schemas). Such rewrites cannot be expressed by the
|
|
93
|
+
* string-level passes at all, so callers use this to require the AST path.
|
|
94
|
+
*/
|
|
95
|
+
hasNameRebinds() {
|
|
96
|
+
return this.nameRebinds().length > 0;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Every object route that rebinds a name or de-qualifies, keyed by source
|
|
100
|
+
* schema and namespace. Callers use this to report or verify substitutions.
|
|
101
|
+
*/
|
|
102
|
+
nameRebinds() {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const [schema, route] of this.routes) {
|
|
105
|
+
for (const ns of ['relation', 'function', 'type']) {
|
|
106
|
+
const bucket = route[NS_BUCKET[ns]];
|
|
107
|
+
if (!bucket)
|
|
108
|
+
continue;
|
|
109
|
+
for (const [from, target] of Object.entries(bucket)) {
|
|
110
|
+
if (typeof target === 'string')
|
|
111
|
+
continue;
|
|
112
|
+
if (target.name === undefined && target.schema !== null)
|
|
113
|
+
continue;
|
|
114
|
+
out.push({ schema, ns, from, to: target });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
77
120
|
/** Every source schema this router may touch. */
|
|
78
121
|
sourceSchemas() {
|
|
79
122
|
return [...this.routes.keys()];
|
|
@@ -85,18 +128,39 @@ class SchemaRouter {
|
|
|
85
128
|
* to any object without a specific route.
|
|
86
129
|
*/
|
|
87
130
|
resolve(sourceSchema, name, ns = 'unknown') {
|
|
131
|
+
// A `null` target de-qualifies the reference; the schema-only API cannot
|
|
132
|
+
// express that, so it reads as "unchanged" here.
|
|
133
|
+
return this.resolveObject(sourceSchema, name, ns)?.schema ?? undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the full target for `(sourceSchema, name)` in namespace `ns` — both
|
|
137
|
+
* the schema the reference should live in and, when the route rebinds, the
|
|
138
|
+
* name it should be reached by. Returns `undefined` to leave it unchanged.
|
|
139
|
+
*
|
|
140
|
+
* In the result, `schema` is `null` when the reference should become
|
|
141
|
+
* unqualified and `undefined` when only the name changes; `name` is
|
|
142
|
+
* `undefined` when only the schema changes.
|
|
143
|
+
*/
|
|
144
|
+
resolveObject(sourceSchema, name, ns = 'unknown') {
|
|
88
145
|
if (!sourceSchema)
|
|
89
146
|
return undefined;
|
|
90
147
|
const route = this.routes.get(sourceSchema);
|
|
91
148
|
if (!route)
|
|
92
149
|
return undefined;
|
|
93
150
|
if (name && (ns === 'relation' || ns === 'function' || ns === 'type')) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
const target = route[NS_BUCKET[ns]]?.[name];
|
|
152
|
+
if (target !== undefined) {
|
|
153
|
+
if (typeof target === 'string')
|
|
154
|
+
return { schema: target };
|
|
155
|
+
// An object route naming no schema inherits the schema-level default,
|
|
156
|
+
// so a pure rebind leaves placement alone.
|
|
157
|
+
return {
|
|
158
|
+
schema: target.schema !== undefined ? target.schema : route.schema,
|
|
159
|
+
name: target.name
|
|
160
|
+
};
|
|
161
|
+
}
|
|
98
162
|
}
|
|
99
|
-
return route.schema;
|
|
163
|
+
return route.schema !== undefined ? { schema: route.schema } : undefined;
|
|
100
164
|
}
|
|
101
165
|
/**
|
|
102
166
|
* Source schemas whose *every* object is guaranteed to move — i.e. those
|
package/transform.js
CHANGED
|
@@ -101,12 +101,24 @@ function transformNameList(names, schemaMapping, result, ns = 'unknown') {
|
|
|
101
101
|
const first = names[0];
|
|
102
102
|
if (first?.String?.sval) {
|
|
103
103
|
const schemaName = first.String.sval;
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
104
|
+
const last = names[names.length - 1];
|
|
105
|
+
const objName = last?.String?.sval;
|
|
106
|
+
const target = router.resolveObject(schemaName, objName, ns);
|
|
107
|
+
if (!target)
|
|
108
|
+
return;
|
|
109
|
+
if (target.name !== undefined && last?.String?.sval) {
|
|
107
110
|
result.schemasFound.add(schemaName);
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
last.String.sval = target.name;
|
|
112
|
+
}
|
|
113
|
+
if (target.schema === null) {
|
|
114
|
+
// De-qualify: drop the schema element and rely on search_path.
|
|
115
|
+
result.schemasFound.add(schemaName);
|
|
116
|
+
names.splice(0, 1);
|
|
117
|
+
}
|
|
118
|
+
else if (target.schema && target.schema !== schemaName) {
|
|
119
|
+
result.schemasFound.add(schemaName);
|
|
120
|
+
first.String.sval = target.schema;
|
|
121
|
+
result.schemasTransformed.set(schemaName, target.schema);
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
}
|
|
@@ -138,11 +150,22 @@ function transformRelation(relation, schemaMapping, result) {
|
|
|
138
150
|
const oldName = relation.schemaname;
|
|
139
151
|
// A RangeVar names a relation (table/view/sequence/matview); route by the
|
|
140
152
|
// relation name so object-level routes can send it to its own schema.
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
153
|
+
const target = asRouter(schemaMapping).resolveObject(oldName, relation.relname, 'relation');
|
|
154
|
+
if (!target)
|
|
155
|
+
return;
|
|
156
|
+
if (target.name !== undefined && relation.relname) {
|
|
157
|
+
result.schemasFound.add(oldName);
|
|
158
|
+
relation.relname = target.name;
|
|
159
|
+
}
|
|
160
|
+
if (target.schema === null) {
|
|
161
|
+
// De-qualify: drop the schema qualifier and rely on search_path.
|
|
162
|
+
result.schemasFound.add(oldName);
|
|
163
|
+
delete relation.schemaname;
|
|
164
|
+
}
|
|
165
|
+
else if (target.schema && target.schema !== oldName) {
|
|
143
166
|
result.schemasFound.add(oldName);
|
|
144
|
-
relation.schemaname =
|
|
145
|
-
result.schemasTransformed.set(oldName,
|
|
167
|
+
relation.schemaname = target.schema;
|
|
168
|
+
result.schemasTransformed.set(oldName, target.schema);
|
|
146
169
|
}
|
|
147
170
|
}
|
|
148
171
|
/**
|