@constructive-io/graphql-server 5.33.0 → 5.33.2

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.
@@ -38,7 +38,7 @@ export const requireApiRole = (column, value, apiId) => {
38
38
  return value;
39
39
  };
40
40
  const RESOLVER_FUNCTION = 'resolve_route';
41
- /** The scoped routing plane the server resolves hosts on first. */
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
- * The database-scope routing planes after the server's own `routing_public` plane, in
78
- * the static gateway's order (by database id). The list spans every tenant,
79
- * so it comes from a SECURITY DEFINER reader granted to the platform login
80
- * alone, on the login's own connection: no role switch, no RLS bypass. A plane
81
- * whose registration names no schema or resolver is an incomplete install and
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 loadDatabaseRoutingPlanes = async (pool) => {
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
- .filter((row) => row.schema_name !== ROUTING_SCHEMA)
89
- .map((row) => {
90
- if (!row.schema_name || !row.resolver_function_name) {
91
- const error = new Error(`route_module for database ${row.database_id} names no ${row.schema_name ? 'resolver function' : 'public schema'}`);
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.schema, plane.resolver);
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` first, then
111
- * each tenant database-scope plane. The first plane that matches owns the
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 route = await resolveRoute(pool, ROUTING_SCHEMA, host);
116
- if (route)
117
- return { route, planeDatabaseId: null };
118
- for (const plane of await loadDatabaseRoutingPlanes(pool)) {
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})`);
@@ -25,7 +25,7 @@ export interface ResolvedRoute {
25
25
  tls_status: string | null;
26
26
  tls_secret_name: string | null;
27
27
  }
28
- /** The scoped routing plane the server resolves hosts on first. */
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 tenant database's own database-scope routing plane: one `route_module`
45
- * row whose resolver keeps the resolve_route contract.
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
- * The database-scope routing planes after the server's own `routing_public` plane, in
54
- * the static gateway's order (by database id). The list spans every tenant,
55
- * so it comes from a SECURITY DEFINER reader granted to the platform login
56
- * alone, on the login's own connection: no role switch, no RLS bypass. A plane
57
- * whose registration names no schema or resolver is an incomplete install and
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 loadDatabaseRoutingPlanes: (pool: Pool) => Promise<DatabaseRoutingPlane[]>;
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` first, then
68
- * each tenant database-scope plane. The first plane that matches owns the
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
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.routeToApiStructure = exports.resolveRouteAcrossPlanes = exports.loadDatabaseRoutingPlanes = exports.resolveRoute = exports.isValidSchemaName = exports.ROUTING_SCHEMA = exports.requireApiRole = void 0;
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
- /** The scoped routing plane the server resolves hosts on first. */
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
- * The database-scope routing planes after the server's own `routing_public` plane, in
84
- * the static gateway's order (by database id). The list spans every tenant,
85
- * so it comes from a SECURITY DEFINER reader granted to the platform login
86
- * alone, on the login's own connection: no role switch, no RLS bypass. A plane
87
- * whose registration names no schema or resolver is an incomplete install and
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 loadDatabaseRoutingPlanes = async (pool) => {
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
- .filter((row) => row.schema_name !== exports.ROUTING_SCHEMA)
95
- .map((row) => {
96
- if (!row.schema_name || !row.resolver_function_name) {
97
- const error = new Error(`route_module for database ${row.database_id} names no ${row.schema_name ? 'resolver function' : 'public schema'}`);
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.loadDatabaseRoutingPlanes = loadDatabaseRoutingPlanes;
114
+ exports.loadRoutingPlanes = loadRoutingPlanes;
110
115
  const resolveOnPlane = async (pool, plane, host) => {
111
- const resolver = quotes_1.QuoteUtils.quoteQualifiedIdentifier(plane.schema, plane.resolver);
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` first, then
118
- * each tenant database-scope plane. The first plane that matches owns the
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 route = await (0, exports.resolveRoute)(pool, exports.ROUTING_SCHEMA, host);
123
- if (route)
124
- return { route, planeDatabaseId: null };
125
- for (const plane of await (0, exports.loadDatabaseRoutingPlanes)(pool)) {
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.0",
3
+ "version": "5.33.2",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Constructive GraphQL Server",
6
6
  "main": "index.js",
@@ -70,9 +70,9 @@
70
70
  "graphile-config": "1.1.0",
71
71
  "graphile-function-bindings": "^1.19.0",
72
72
  "graphile-plugin-utils": "^1.14.1",
73
- "graphile-presigned-url-plugin": "^1.23.3",
73
+ "graphile-presigned-url-plugin": "^1.23.4",
74
74
  "graphile-scoped-introspection": "^0.6.0",
75
- "graphile-settings": "^6.28.0",
75
+ "graphile-settings": "^6.28.1",
76
76
  "graphile-utils": "5.0.3",
77
77
  "graphql": "16.13.0",
78
78
  "graphql-upload": "^13.0.0",
@@ -100,5 +100,5 @@
100
100
  "supertest": "^7.2.2",
101
101
  "ts-node": "^10.9.2"
102
102
  },
103
- "gitHead": "55dbe288f7f011fa6987f7f99bb9fc8d7183152b"
103
+ "gitHead": "0e90e47d0abcc37fb74ae98ed9cff5ada43c68e2"
104
104
  }