@constructive-io/graphql-server 5.33.0 → 5.33.1
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/middleware/routing.js +33 -22
- package/middleware/routing.d.ts +14 -13
- package/middleware/routing.js +35 -24
- package/package.json +2 -2
|
@@ -38,7 +38,7 @@ export const requireApiRole = (column, value, apiId) => {
|
|
|
38
38
|
return value;
|
|
39
39
|
};
|
|
40
40
|
const RESOLVER_FUNCTION = 'resolve_route';
|
|
41
|
-
/**
|
|
41
|
+
/** API schema of the routing plane the server resolves hosts on first. */
|
|
42
42
|
export const ROUTING_SCHEMA = 'routing_public';
|
|
43
43
|
export const isValidSchemaName = (name) => /^[a-z_][a-z0-9_]*$/.test(name);
|
|
44
44
|
/**
|
|
@@ -74,21 +74,25 @@ export const resolveRoute = async (pool, schema, host) => {
|
|
|
74
74
|
}
|
|
75
75
|
};
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* fails loud.
|
|
77
|
+
* Every database-scope routing plane, in the static gateway's order (by
|
|
78
|
+
* database id). The list spans every tenant, so it comes from a SECURITY
|
|
79
|
+
* DEFINER reader granted to the platform login alone, on the login's own
|
|
80
|
+
* connection: no role switch, no RLS bypass. A plane whose registration names
|
|
81
|
+
* no schema or resolver is an incomplete install and fails loud.
|
|
83
82
|
*/
|
|
84
|
-
export const
|
|
85
|
-
const { rows } = await pool.query(`SELECT database_id, schema_name, resolver_function_name
|
|
83
|
+
export const loadRoutingPlanes = async (pool) => {
|
|
84
|
+
const { rows } = await pool.query(`SELECT database_id, schema_name, resolver_schema_name, resolver_function_name
|
|
86
85
|
FROM function_resolution.routing_planes()`);
|
|
87
|
-
return rows
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
return rows.map((row) => {
|
|
87
|
+
const missing = !row.schema_name
|
|
88
|
+
? 'public schema'
|
|
89
|
+
: !row.resolver_schema_name
|
|
90
|
+
? 'resolver schema'
|
|
91
|
+
: !row.resolver_function_name
|
|
92
|
+
? 'resolver function'
|
|
93
|
+
: null;
|
|
94
|
+
if (missing) {
|
|
95
|
+
const error = new Error(`route_module for database ${row.database_id} names no ${missing}`);
|
|
92
96
|
error.code = 'ROUTING_PLANE_INCOMPLETE';
|
|
93
97
|
error.databaseId = row.database_id;
|
|
94
98
|
throw error;
|
|
@@ -96,26 +100,33 @@ export const loadDatabaseRoutingPlanes = async (pool) => {
|
|
|
96
100
|
return {
|
|
97
101
|
databaseId: row.database_id,
|
|
98
102
|
schema: row.schema_name,
|
|
103
|
+
resolverSchema: row.resolver_schema_name,
|
|
99
104
|
resolver: row.resolver_function_name
|
|
100
105
|
};
|
|
101
106
|
});
|
|
102
107
|
};
|
|
103
108
|
const resolveOnPlane = async (pool, plane, host) => {
|
|
104
|
-
const resolver = QuoteUtils.quoteQualifiedIdentifier(plane.
|
|
109
|
+
const resolver = QuoteUtils.quoteQualifiedIdentifier(plane.resolverSchema, plane.resolver);
|
|
105
110
|
const result = await pool.query(`SELECT * FROM ${resolver}($1, '/', NULL)`, [host]);
|
|
106
111
|
const row = result.rows[0];
|
|
107
112
|
return row && row.route_binding_id !== null ? row : null;
|
|
108
113
|
};
|
|
109
114
|
/**
|
|
110
|
-
* Resolve a host across routing planes: `routing_public`
|
|
111
|
-
* each tenant database-scope plane. The first plane that
|
|
112
|
-
* host; later planes are not consulted.
|
|
115
|
+
* Resolve a host across routing planes: the server's own `routing_public`
|
|
116
|
+
* plane first, then each tenant database-scope plane. The first plane that
|
|
117
|
+
* matches owns the host; later planes are not consulted.
|
|
113
118
|
*/
|
|
114
119
|
export const resolveRouteAcrossPlanes = async (pool, host) => {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
const planes = await loadRoutingPlanes(pool);
|
|
121
|
+
const own = planes.find((plane) => plane.schema === ROUTING_SCHEMA);
|
|
122
|
+
if (own) {
|
|
123
|
+
const route = await resolveRoute(pool, own.resolverSchema, host);
|
|
124
|
+
if (route)
|
|
125
|
+
return { route, planeDatabaseId: null };
|
|
126
|
+
}
|
|
127
|
+
for (const plane of planes) {
|
|
128
|
+
if (plane === own)
|
|
129
|
+
continue;
|
|
119
130
|
const planeRoute = await resolveOnPlane(pool, plane, host);
|
|
120
131
|
if (planeRoute) {
|
|
121
132
|
log.debug(`[resolve-route] host=${host} owned by database plane ${plane.schema} (db=${plane.databaseId})`);
|
package/middleware/routing.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface ResolvedRoute {
|
|
|
25
25
|
tls_status: string | null;
|
|
26
26
|
tls_secret_name: string | null;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
28
|
+
/** API schema of the routing plane the server resolves hosts on first. */
|
|
29
29
|
export declare const ROUTING_SCHEMA = "routing_public";
|
|
30
30
|
export declare const isValidSchemaName: (name: string) => boolean;
|
|
31
31
|
/**
|
|
@@ -41,32 +41,33 @@ export declare const resolveRoute: (pool: Pool, schema: string, host: string) =>
|
|
|
41
41
|
* `resolved_config`) that the server maps onto ApiStructure.
|
|
42
42
|
*/
|
|
43
43
|
/**
|
|
44
|
-
* A
|
|
45
|
-
*
|
|
44
|
+
* A database-scope routing plane: one `route_module` row whose resolver keeps
|
|
45
|
+
* the resolve_route contract. `schema` is the plane's API schema (it names the
|
|
46
|
+
* server's own plane); `resolverSchema` is where its resolver lives.
|
|
46
47
|
*/
|
|
47
48
|
export interface DatabaseRoutingPlane {
|
|
48
49
|
databaseId: string;
|
|
49
50
|
schema: string;
|
|
51
|
+
resolverSchema: string;
|
|
50
52
|
resolver: string;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* fails loud.
|
|
55
|
+
* Every database-scope routing plane, in the static gateway's order (by
|
|
56
|
+
* database id). The list spans every tenant, so it comes from a SECURITY
|
|
57
|
+
* DEFINER reader granted to the platform login alone, on the login's own
|
|
58
|
+
* connection: no role switch, no RLS bypass. A plane whose registration names
|
|
59
|
+
* no schema or resolver is an incomplete install and fails loud.
|
|
59
60
|
*/
|
|
60
|
-
export declare const
|
|
61
|
+
export declare const loadRoutingPlanes: (pool: Pool) => Promise<DatabaseRoutingPlane[]>;
|
|
61
62
|
export interface PlaneRouteMatch {
|
|
62
63
|
route: ResolvedRoute;
|
|
63
64
|
/** Database whose own plane owns the host; null for `routing_public`. */
|
|
64
65
|
planeDatabaseId: string | null;
|
|
65
66
|
}
|
|
66
67
|
/**
|
|
67
|
-
* Resolve a host across routing planes: `routing_public`
|
|
68
|
-
* each tenant database-scope plane. The first plane that
|
|
69
|
-
* host; later planes are not consulted.
|
|
68
|
+
* Resolve a host across routing planes: the server's own `routing_public`
|
|
69
|
+
* plane first, then each tenant database-scope plane. The first plane that
|
|
70
|
+
* matches owns the host; later planes are not consulted.
|
|
70
71
|
*/
|
|
71
72
|
export declare const resolveRouteAcrossPlanes: (pool: Pool, host: string) => Promise<PlaneRouteMatch | null>;
|
|
72
73
|
/**
|
package/middleware/routing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.routeToApiStructure = exports.resolveRouteAcrossPlanes = exports.
|
|
3
|
+
exports.routeToApiStructure = exports.resolveRouteAcrossPlanes = exports.loadRoutingPlanes = exports.resolveRoute = exports.isValidSchemaName = exports.ROUTING_SCHEMA = exports.requireApiRole = void 0;
|
|
4
4
|
const logger_1 = require("@pgpmjs/logger");
|
|
5
5
|
const quotes_1 = require("@pgsql/quotes");
|
|
6
6
|
const log = new logger_1.Logger('routing');
|
|
@@ -42,7 +42,7 @@ const requireApiRole = (column, value, apiId) => {
|
|
|
42
42
|
};
|
|
43
43
|
exports.requireApiRole = requireApiRole;
|
|
44
44
|
const RESOLVER_FUNCTION = 'resolve_route';
|
|
45
|
-
/**
|
|
45
|
+
/** API schema of the routing plane the server resolves hosts on first. */
|
|
46
46
|
exports.ROUTING_SCHEMA = 'routing_public';
|
|
47
47
|
const isValidSchemaName = (name) => /^[a-z_][a-z0-9_]*$/.test(name);
|
|
48
48
|
exports.isValidSchemaName = isValidSchemaName;
|
|
@@ -80,21 +80,25 @@ const resolveRoute = async (pool, schema, host) => {
|
|
|
80
80
|
};
|
|
81
81
|
exports.resolveRoute = resolveRoute;
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* fails loud.
|
|
83
|
+
* Every database-scope routing plane, in the static gateway's order (by
|
|
84
|
+
* database id). The list spans every tenant, so it comes from a SECURITY
|
|
85
|
+
* DEFINER reader granted to the platform login alone, on the login's own
|
|
86
|
+
* connection: no role switch, no RLS bypass. A plane whose registration names
|
|
87
|
+
* no schema or resolver is an incomplete install and fails loud.
|
|
89
88
|
*/
|
|
90
|
-
const
|
|
91
|
-
const { rows } = await pool.query(`SELECT database_id, schema_name, resolver_function_name
|
|
89
|
+
const loadRoutingPlanes = async (pool) => {
|
|
90
|
+
const { rows } = await pool.query(`SELECT database_id, schema_name, resolver_schema_name, resolver_function_name
|
|
92
91
|
FROM function_resolution.routing_planes()`);
|
|
93
|
-
return rows
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
return rows.map((row) => {
|
|
93
|
+
const missing = !row.schema_name
|
|
94
|
+
? 'public schema'
|
|
95
|
+
: !row.resolver_schema_name
|
|
96
|
+
? 'resolver schema'
|
|
97
|
+
: !row.resolver_function_name
|
|
98
|
+
? 'resolver function'
|
|
99
|
+
: null;
|
|
100
|
+
if (missing) {
|
|
101
|
+
const error = new Error(`route_module for database ${row.database_id} names no ${missing}`);
|
|
98
102
|
error.code = 'ROUTING_PLANE_INCOMPLETE';
|
|
99
103
|
error.databaseId = row.database_id;
|
|
100
104
|
throw error;
|
|
@@ -102,27 +106,34 @@ const loadDatabaseRoutingPlanes = async (pool) => {
|
|
|
102
106
|
return {
|
|
103
107
|
databaseId: row.database_id,
|
|
104
108
|
schema: row.schema_name,
|
|
109
|
+
resolverSchema: row.resolver_schema_name,
|
|
105
110
|
resolver: row.resolver_function_name
|
|
106
111
|
};
|
|
107
112
|
});
|
|
108
113
|
};
|
|
109
|
-
exports.
|
|
114
|
+
exports.loadRoutingPlanes = loadRoutingPlanes;
|
|
110
115
|
const resolveOnPlane = async (pool, plane, host) => {
|
|
111
|
-
const resolver = quotes_1.QuoteUtils.quoteQualifiedIdentifier(plane.
|
|
116
|
+
const resolver = quotes_1.QuoteUtils.quoteQualifiedIdentifier(plane.resolverSchema, plane.resolver);
|
|
112
117
|
const result = await pool.query(`SELECT * FROM ${resolver}($1, '/', NULL)`, [host]);
|
|
113
118
|
const row = result.rows[0];
|
|
114
119
|
return row && row.route_binding_id !== null ? row : null;
|
|
115
120
|
};
|
|
116
121
|
/**
|
|
117
|
-
* Resolve a host across routing planes: `routing_public`
|
|
118
|
-
* each tenant database-scope plane. The first plane that
|
|
119
|
-
* host; later planes are not consulted.
|
|
122
|
+
* Resolve a host across routing planes: the server's own `routing_public`
|
|
123
|
+
* plane first, then each tenant database-scope plane. The first plane that
|
|
124
|
+
* matches owns the host; later planes are not consulted.
|
|
120
125
|
*/
|
|
121
126
|
const resolveRouteAcrossPlanes = async (pool, host) => {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
const planes = await (0, exports.loadRoutingPlanes)(pool);
|
|
128
|
+
const own = planes.find((plane) => plane.schema === exports.ROUTING_SCHEMA);
|
|
129
|
+
if (own) {
|
|
130
|
+
const route = await (0, exports.resolveRoute)(pool, own.resolverSchema, host);
|
|
131
|
+
if (route)
|
|
132
|
+
return { route, planeDatabaseId: null };
|
|
133
|
+
}
|
|
134
|
+
for (const plane of planes) {
|
|
135
|
+
if (plane === own)
|
|
136
|
+
continue;
|
|
126
137
|
const planeRoute = await resolveOnPlane(pool, plane, host);
|
|
127
138
|
if (planeRoute) {
|
|
128
139
|
log.debug(`[resolve-route] host=${host} owned by database plane ${plane.schema} (db=${plane.databaseId})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "5.33.
|
|
3
|
+
"version": "5.33.1",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"supertest": "^7.2.2",
|
|
101
101
|
"ts-node": "^10.9.2"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "69c7d68237a248bf5923dd6e3e3e5d73674c4050"
|
|
104
104
|
}
|