@constructive-io/graphql-server 5.3.3 → 5.4.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 +3 -3
- package/esm/middleware/api.js +14 -10
- package/esm/middleware/cors.js +2 -6
- package/esm/middleware/flush.js +7 -1
- package/esm/middleware/routing.js +5 -2
- package/esm/server.js +7 -2
- package/middleware/api.js +13 -9
- package/middleware/cors.d.ts +0 -1
- package/middleware/cors.js +2 -6
- package/middleware/flush.js +7 -1
- package/middleware/routing.d.ts +9 -0
- package/middleware/routing.js +8 -3
- package/package.json +9 -9
- package/server.js +7 -2
- package/types.d.ts +1 -1
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ For the operational workflow, sampler output, and heap snapshot usage, see [docs
|
|
|
103
103
|
|
|
104
104
|
This is a production-only server: every request is resolved through the scoped-routing plane. There is no static single-tenant mode and no flag to disable routing. For single-database local development without route resolution or a database id, use [`@constructive-io/graphql-dev-server`](../dev-server/README.md).
|
|
105
105
|
|
|
106
|
-
- The server resolves the request host with a single `resolve_route()` call against the compiled route bindings in the scoped routing schema (`
|
|
106
|
+
- The server resolves the request host with a single `resolve_route()` call against the compiled route bindings in the scoped routing schema (`API_ROUTING_SCHEMA`, default `routing_public`), mapping host → tenant/api/database/role.
|
|
107
107
|
- Only APIs where `api.is_public` matches `API_IS_PUBLIC` are served.
|
|
108
108
|
- In private mode (`API_IS_PUBLIC=false`), you can override with headers:
|
|
109
109
|
- `X-Api-Name` + `X-Database-Id`
|
|
@@ -126,10 +126,10 @@ Configuration is merged from defaults, config files, and env vars via `@construc
|
|
|
126
126
|
| `FEATURES_SIMPLE_INFLECTION` | Enable simple inflection | `true` |
|
|
127
127
|
| `FEATURES_OPPOSITE_BASE_NAMES` | Enable opposite base names | `true` |
|
|
128
128
|
| `FEATURES_POSTGIS` | Enable PostGIS support | `true` |
|
|
129
|
-
| `
|
|
129
|
+
| `API_ROUTING_SCHEMA` | Schema containing `resolve_route()` | `routing_public` |
|
|
130
130
|
| `API_IS_PUBLIC` | Serve public APIs only | `true` |
|
|
131
131
|
| `API_EXPOSED_SCHEMAS` | Additional schemas to expose | empty |
|
|
132
|
-
| `API_META_SCHEMAS` | Meta schemas to query | `
|
|
132
|
+
| `API_META_SCHEMAS` | Meta schemas to query | `routing_public,metaschema_public,metaschema_modules_public` |
|
|
133
133
|
| `API_ANON_ROLE` | Anonymous role name | `administrator` |
|
|
134
134
|
| `API_ROLE_NAME` | Authenticated role name | `administrator` |
|
|
135
135
|
| `GRAPHQL_OBSERVABILITY_ENABLED` | Master switch for debug routes and sampler | `false` |
|
package/esm/middleware/api.js
CHANGED
|
@@ -6,7 +6,7 @@ import { svcCache } from '@pgpmjs/server-utils';
|
|
|
6
6
|
import { getPgPool } from 'pg-cache';
|
|
7
7
|
import errorPage50x from '../errors/50x';
|
|
8
8
|
import errorPage404Message from '../errors/404-message';
|
|
9
|
-
import { resolveRoute, routeToApiStructure } from './routing';
|
|
9
|
+
import { getRoutingSchema, isValidSchemaName, resolveRoute, routeToApiStructure } from './routing';
|
|
10
10
|
const log = new Logger('api');
|
|
11
11
|
// =============================================================================
|
|
12
12
|
// Module Loader Registry (replaces inline SQL queries for per-db config)
|
|
@@ -17,7 +17,7 @@ const defaultRegistry = createDefaultRegistry();
|
|
|
17
17
|
// =============================================================================
|
|
18
18
|
// Private-header X-Api-Name lookup against the scoped routing plane.
|
|
19
19
|
// `is_published` is the routing-plane analog of the legacy `is_public` column.
|
|
20
|
-
const
|
|
20
|
+
const scopedApiNameLookupSql = (routingSchema) => `
|
|
21
21
|
SELECT
|
|
22
22
|
a.id as api_id,
|
|
23
23
|
a.database_id,
|
|
@@ -26,8 +26,8 @@ const SCOPED_API_NAME_LOOKUP_SQL = `
|
|
|
26
26
|
a.anon_role,
|
|
27
27
|
a.is_published as is_public,
|
|
28
28
|
COALESCE(array_agg(s.schema_name) FILTER (WHERE s.schema_name IS NOT NULL), '{}') as schemas
|
|
29
|
-
FROM
|
|
30
|
-
LEFT JOIN
|
|
29
|
+
FROM "${routingSchema}".apis a
|
|
30
|
+
LEFT JOIN "${routingSchema}".api_schemas aps ON a.id = aps.api_id
|
|
31
31
|
LEFT JOIN metaschema_public.schema s ON aps.schema_id = s.id
|
|
32
32
|
WHERE a.database_id = $1
|
|
33
33
|
AND a.name = $2
|
|
@@ -41,6 +41,7 @@ const SCOPED_API_NAME_LOOKUP_SQL = `
|
|
|
41
41
|
*/
|
|
42
42
|
const buildLoaderContext = (routingPool, opts, row) => ({
|
|
43
43
|
routingPool,
|
|
44
|
+
routingSchema: getRoutingSchema(opts),
|
|
44
45
|
tenantPool: getPgPool({ ...opts.pg, database: row.dbname }),
|
|
45
46
|
databaseId: row.database_id,
|
|
46
47
|
apiId: row.api_id,
|
|
@@ -136,7 +137,6 @@ const toApiStructure = (row, opts, settings = {}) => ({
|
|
|
136
137
|
anonRole: row.anon_role || 'anon',
|
|
137
138
|
roleName: row.role_name || 'authenticated',
|
|
138
139
|
schema: row.schemas || [],
|
|
139
|
-
apiModules: [],
|
|
140
140
|
rlsModule: settings.rlsModule,
|
|
141
141
|
domains: [],
|
|
142
142
|
databaseId: row.database_id,
|
|
@@ -152,7 +152,6 @@ const createAdminStructure = (opts, schemas, databaseId) => ({
|
|
|
152
152
|
anonRole: 'administrator',
|
|
153
153
|
roleName: 'administrator',
|
|
154
154
|
schema: schemas,
|
|
155
|
-
apiModules: [],
|
|
156
155
|
domains: [],
|
|
157
156
|
databaseId,
|
|
158
157
|
isPublic: false
|
|
@@ -164,8 +163,13 @@ const validateSchemata = async (pool, schemas) => {
|
|
|
164
163
|
const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemas]);
|
|
165
164
|
return result.rows.map((row) => row.schema_name);
|
|
166
165
|
};
|
|
167
|
-
const queryByApiName = async (pool, databaseId, name, isPublic) => {
|
|
168
|
-
const
|
|
166
|
+
const queryByApiName = async (pool, opts, databaseId, name, isPublic) => {
|
|
167
|
+
const routingSchema = getRoutingSchema(opts);
|
|
168
|
+
if (!isValidSchemaName(routingSchema)) {
|
|
169
|
+
log.warn(`[api-name-lookup] invalid routing schema name: ${routingSchema}`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const result = await pool.query(scopedApiNameLookupSql(routingSchema), [databaseId, name, isPublic]);
|
|
169
173
|
return result.rows[0] ?? null;
|
|
170
174
|
};
|
|
171
175
|
// =============================================================================
|
|
@@ -193,7 +197,7 @@ const resolveApiNameHeader = async (ctx) => {
|
|
|
193
197
|
if (!headers.databaseId)
|
|
194
198
|
return null;
|
|
195
199
|
const isPublic = opts.api?.isPublic ?? false;
|
|
196
|
-
const row = await queryByApiName(pool, headers.databaseId, headers.apiName, isPublic);
|
|
200
|
+
const row = await queryByApiName(pool, opts, headers.databaseId, headers.apiName, isPublic);
|
|
197
201
|
if (!row) {
|
|
198
202
|
log.debug(`[api-name-lookup] No API found for databaseId=${headers.databaseId} name=${headers.apiName}`);
|
|
199
203
|
return null;
|
|
@@ -216,7 +220,7 @@ const resolveMetaSchemaHeader = (ctx, validatedSchemas) => {
|
|
|
216
220
|
*/
|
|
217
221
|
const resolveScopedRoute = async (ctx) => {
|
|
218
222
|
const { opts, pool, host } = ctx;
|
|
219
|
-
const schema = opts
|
|
223
|
+
const schema = getRoutingSchema(opts);
|
|
220
224
|
const route = await resolveRoute(pool, schema, host);
|
|
221
225
|
if (!route)
|
|
222
226
|
return null;
|
package/esm/middleware/cors.js
CHANGED
|
@@ -7,7 +7,6 @@ import './types'; // for Request type
|
|
|
7
7
|
* Feature parity + compatibility:
|
|
8
8
|
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
9
9
|
* - Reads per-API CORS origins from typed cors_settings table (via req.api.corsOrigins).
|
|
10
|
-
* - Falls back to legacy api_modules CORS data for backwards compatibility.
|
|
11
10
|
* - Always allows localhost to ease development.
|
|
12
11
|
*
|
|
13
12
|
* Usage:
|
|
@@ -32,13 +31,10 @@ export const cors = (fallbackOrigin) => {
|
|
|
32
31
|
// createApiMiddleware runs before this in server.ts, so req.api should be set
|
|
33
32
|
const api = req.api;
|
|
34
33
|
if (api) {
|
|
35
|
-
// Typed cors_settings origins
|
|
34
|
+
// Typed cors_settings origins
|
|
36
35
|
const typedOrigins = api.corsOrigins || [];
|
|
37
|
-
// Legacy api_modules CORS data (fallback)
|
|
38
|
-
const corsModules = (api.apiModules || []).filter((m) => m.name === 'cors');
|
|
39
|
-
const legacyOrigins = corsModules.reduce((m, mod) => [...mod.data.urls, ...m], []);
|
|
40
36
|
const siteUrls = api.domains || [];
|
|
41
|
-
const listOfDomains = [...typedOrigins, ...
|
|
37
|
+
const listOfDomains = [...typedOrigins, ...siteUrls];
|
|
42
38
|
if (origin && listOfDomains.includes(origin)) {
|
|
43
39
|
return callback(null, true);
|
|
44
40
|
}
|
package/esm/middleware/flush.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Logger } from '@pgpmjs/logger';
|
|
|
3
3
|
import { svcCache } from '@pgpmjs/server-utils';
|
|
4
4
|
import { graphileCache } from 'graphile-cache';
|
|
5
5
|
import { getPgPool } from 'pg-cache';
|
|
6
|
+
import { getRoutingSchema, isValidSchemaName } from './routing';
|
|
6
7
|
const log = new Logger('flush');
|
|
7
8
|
export const flush = async (req, res, next) => {
|
|
8
9
|
if (req.url === '/flush') {
|
|
@@ -28,8 +29,13 @@ export const flushService = async (opts, databaseId) => {
|
|
|
28
29
|
}
|
|
29
30
|
});
|
|
30
31
|
}
|
|
32
|
+
const routingSchema = getRoutingSchema(opts);
|
|
33
|
+
if (!isValidSchemaName(routingSchema)) {
|
|
34
|
+
log.warn(`[flush] invalid routing schema name: ${routingSchema}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
31
37
|
const svc = await pgPool.query(`SELECT hostname
|
|
32
|
-
FROM
|
|
38
|
+
FROM "${routingSchema}".domains
|
|
33
39
|
WHERE database_id = $1`, [databaseId]);
|
|
34
40
|
if (svc.rowCount === 0)
|
|
35
41
|
return;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Logger } from '@pgpmjs/logger';
|
|
2
2
|
const log = new Logger('routing');
|
|
3
3
|
const RESOLVER_FUNCTION = 'resolve_route';
|
|
4
|
-
|
|
4
|
+
/** Published logical name of the database-scope routing plane. */
|
|
5
|
+
export const DEFAULT_ROUTING_SCHEMA = 'routing_public';
|
|
6
|
+
/** The routing-plane schema in effect: configured override or the published default. */
|
|
7
|
+
export const getRoutingSchema = (opts) => opts.api?.routingSchema || DEFAULT_ROUTING_SCHEMA;
|
|
8
|
+
export const isValidSchemaName = (name) => /^[a-z_][a-z0-9_]*$/.test(name);
|
|
5
9
|
/**
|
|
6
10
|
* Resolve a hostname through the compiled scoped-routing plane (host-only:
|
|
7
11
|
* path/method routing belongs to Traefik/Ingress, not the server).
|
|
@@ -54,7 +58,6 @@ export const routeToApiStructure = (route, opts) => {
|
|
|
54
58
|
anonRole: config.anon_role || 'anon',
|
|
55
59
|
roleName: config.role_name || 'authenticated',
|
|
56
60
|
schema: config.schemas,
|
|
57
|
-
apiModules: [],
|
|
58
61
|
domains: [],
|
|
59
62
|
databaseId: config.database_id,
|
|
60
63
|
isPublic: config.is_public ?? (opts.api?.isPublic ?? false)
|
package/esm/server.js
CHANGED
|
@@ -30,6 +30,7 @@ import { createDebugDatabaseMiddleware } from './middleware/observability/debug-
|
|
|
30
30
|
import { debugMemory } from './middleware/observability/debug-memory';
|
|
31
31
|
import { localObservabilityOnly } from './middleware/observability/guard';
|
|
32
32
|
import { createRequestLogger } from './middleware/observability/request-logger';
|
|
33
|
+
import { getRoutingSchema } from './middleware/routing';
|
|
33
34
|
const log = new Logger('server');
|
|
34
35
|
/**
|
|
35
36
|
* Creates and starts a GraphQL server instance
|
|
@@ -85,7 +86,7 @@ class Server {
|
|
|
85
86
|
serverHost: effectiveOpts.server?.host,
|
|
86
87
|
serverPort: effectiveOpts.server?.port,
|
|
87
88
|
apiIsPublic: apiOpts.isPublic,
|
|
88
|
-
|
|
89
|
+
routingSchema: apiOpts.routingSchema,
|
|
89
90
|
metaSchemas: apiOpts.metaSchemas?.join(',') || 'default',
|
|
90
91
|
observabilityEnabled
|
|
91
92
|
});
|
|
@@ -136,7 +137,11 @@ class Server {
|
|
|
136
137
|
app.use(requestLogger);
|
|
137
138
|
app.use(api);
|
|
138
139
|
app.use(authenticate);
|
|
139
|
-
app.use(createContextMiddleware({
|
|
140
|
+
app.use(createContextMiddleware({
|
|
141
|
+
pg: effectiveOpts.pg,
|
|
142
|
+
loaders: createDefaultRegistry(),
|
|
143
|
+
routingSchema: getRoutingSchema(effectiveOpts)
|
|
144
|
+
}));
|
|
140
145
|
app.use(createCaptchaMiddleware());
|
|
141
146
|
// CSRF protection for cookie-authenticated requests
|
|
142
147
|
// Skip CSRF for Bearer token auth (not vulnerable to CSRF) and anonymous requests
|
package/middleware/api.js
CHANGED
|
@@ -23,7 +23,7 @@ const defaultRegistry = (0, express_context_1.createDefaultRegistry)();
|
|
|
23
23
|
// =============================================================================
|
|
24
24
|
// Private-header X-Api-Name lookup against the scoped routing plane.
|
|
25
25
|
// `is_published` is the routing-plane analog of the legacy `is_public` column.
|
|
26
|
-
const
|
|
26
|
+
const scopedApiNameLookupSql = (routingSchema) => `
|
|
27
27
|
SELECT
|
|
28
28
|
a.id as api_id,
|
|
29
29
|
a.database_id,
|
|
@@ -32,8 +32,8 @@ const SCOPED_API_NAME_LOOKUP_SQL = `
|
|
|
32
32
|
a.anon_role,
|
|
33
33
|
a.is_published as is_public,
|
|
34
34
|
COALESCE(array_agg(s.schema_name) FILTER (WHERE s.schema_name IS NOT NULL), '{}') as schemas
|
|
35
|
-
FROM
|
|
36
|
-
LEFT JOIN
|
|
35
|
+
FROM "${routingSchema}".apis a
|
|
36
|
+
LEFT JOIN "${routingSchema}".api_schemas aps ON a.id = aps.api_id
|
|
37
37
|
LEFT JOIN metaschema_public.schema s ON aps.schema_id = s.id
|
|
38
38
|
WHERE a.database_id = $1
|
|
39
39
|
AND a.name = $2
|
|
@@ -47,6 +47,7 @@ const SCOPED_API_NAME_LOOKUP_SQL = `
|
|
|
47
47
|
*/
|
|
48
48
|
const buildLoaderContext = (routingPool, opts, row) => ({
|
|
49
49
|
routingPool,
|
|
50
|
+
routingSchema: (0, routing_1.getRoutingSchema)(opts),
|
|
50
51
|
tenantPool: (0, pg_cache_1.getPgPool)({ ...opts.pg, database: row.dbname }),
|
|
51
52
|
databaseId: row.database_id,
|
|
52
53
|
apiId: row.api_id,
|
|
@@ -144,7 +145,6 @@ const toApiStructure = (row, opts, settings = {}) => ({
|
|
|
144
145
|
anonRole: row.anon_role || 'anon',
|
|
145
146
|
roleName: row.role_name || 'authenticated',
|
|
146
147
|
schema: row.schemas || [],
|
|
147
|
-
apiModules: [],
|
|
148
148
|
rlsModule: settings.rlsModule,
|
|
149
149
|
domains: [],
|
|
150
150
|
databaseId: row.database_id,
|
|
@@ -160,7 +160,6 @@ const createAdminStructure = (opts, schemas, databaseId) => ({
|
|
|
160
160
|
anonRole: 'administrator',
|
|
161
161
|
roleName: 'administrator',
|
|
162
162
|
schema: schemas,
|
|
163
|
-
apiModules: [],
|
|
164
163
|
domains: [],
|
|
165
164
|
databaseId,
|
|
166
165
|
isPublic: false
|
|
@@ -172,8 +171,13 @@ const validateSchemata = async (pool, schemas) => {
|
|
|
172
171
|
const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemas]);
|
|
173
172
|
return result.rows.map((row) => row.schema_name);
|
|
174
173
|
};
|
|
175
|
-
const queryByApiName = async (pool, databaseId, name, isPublic) => {
|
|
176
|
-
const
|
|
174
|
+
const queryByApiName = async (pool, opts, databaseId, name, isPublic) => {
|
|
175
|
+
const routingSchema = (0, routing_1.getRoutingSchema)(opts);
|
|
176
|
+
if (!(0, routing_1.isValidSchemaName)(routingSchema)) {
|
|
177
|
+
log.warn(`[api-name-lookup] invalid routing schema name: ${routingSchema}`);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const result = await pool.query(scopedApiNameLookupSql(routingSchema), [databaseId, name, isPublic]);
|
|
177
181
|
return result.rows[0] ?? null;
|
|
178
182
|
};
|
|
179
183
|
// =============================================================================
|
|
@@ -201,7 +205,7 @@ const resolveApiNameHeader = async (ctx) => {
|
|
|
201
205
|
if (!headers.databaseId)
|
|
202
206
|
return null;
|
|
203
207
|
const isPublic = opts.api?.isPublic ?? false;
|
|
204
|
-
const row = await queryByApiName(pool, headers.databaseId, headers.apiName, isPublic);
|
|
208
|
+
const row = await queryByApiName(pool, opts, headers.databaseId, headers.apiName, isPublic);
|
|
205
209
|
if (!row) {
|
|
206
210
|
log.debug(`[api-name-lookup] No API found for databaseId=${headers.databaseId} name=${headers.apiName}`);
|
|
207
211
|
return null;
|
|
@@ -224,7 +228,7 @@ const resolveMetaSchemaHeader = (ctx, validatedSchemas) => {
|
|
|
224
228
|
*/
|
|
225
229
|
const resolveScopedRoute = async (ctx) => {
|
|
226
230
|
const { opts, pool, host } = ctx;
|
|
227
|
-
const schema = opts
|
|
231
|
+
const schema = (0, routing_1.getRoutingSchema)(opts);
|
|
228
232
|
const route = await (0, routing_1.resolveRoute)(pool, schema, host);
|
|
229
233
|
if (!route)
|
|
230
234
|
return null;
|
package/middleware/cors.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ import './types';
|
|
|
6
6
|
* Feature parity + compatibility:
|
|
7
7
|
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
8
8
|
* - Reads per-API CORS origins from typed cors_settings table (via req.api.corsOrigins).
|
|
9
|
-
* - Falls back to legacy api_modules CORS data for backwards compatibility.
|
|
10
9
|
* - Always allows localhost to ease development.
|
|
11
10
|
*
|
|
12
11
|
* Usage:
|
package/middleware/cors.js
CHANGED
|
@@ -13,7 +13,6 @@ require("./types"); // for Request type
|
|
|
13
13
|
* Feature parity + compatibility:
|
|
14
14
|
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
15
15
|
* - Reads per-API CORS origins from typed cors_settings table (via req.api.corsOrigins).
|
|
16
|
-
* - Falls back to legacy api_modules CORS data for backwards compatibility.
|
|
17
16
|
* - Always allows localhost to ease development.
|
|
18
17
|
*
|
|
19
18
|
* Usage:
|
|
@@ -38,13 +37,10 @@ const cors = (fallbackOrigin) => {
|
|
|
38
37
|
// createApiMiddleware runs before this in server.ts, so req.api should be set
|
|
39
38
|
const api = req.api;
|
|
40
39
|
if (api) {
|
|
41
|
-
// Typed cors_settings origins
|
|
40
|
+
// Typed cors_settings origins
|
|
42
41
|
const typedOrigins = api.corsOrigins || [];
|
|
43
|
-
// Legacy api_modules CORS data (fallback)
|
|
44
|
-
const corsModules = (api.apiModules || []).filter((m) => m.name === 'cors');
|
|
45
|
-
const legacyOrigins = corsModules.reduce((m, mod) => [...mod.data.urls, ...m], []);
|
|
46
42
|
const siteUrls = api.domains || [];
|
|
47
|
-
const listOfDomains = [...typedOrigins, ...
|
|
43
|
+
const listOfDomains = [...typedOrigins, ...siteUrls];
|
|
48
44
|
if (origin && listOfDomains.includes(origin)) {
|
|
49
45
|
return callback(null, true);
|
|
50
46
|
}
|
package/middleware/flush.js
CHANGED
|
@@ -6,6 +6,7 @@ const logger_1 = require("@pgpmjs/logger");
|
|
|
6
6
|
const server_utils_1 = require("@pgpmjs/server-utils");
|
|
7
7
|
const graphile_cache_1 = require("graphile-cache");
|
|
8
8
|
const pg_cache_1 = require("pg-cache");
|
|
9
|
+
const routing_1 = require("./routing");
|
|
9
10
|
const log = new logger_1.Logger('flush');
|
|
10
11
|
const flush = async (req, res, next) => {
|
|
11
12
|
if (req.url === '/flush') {
|
|
@@ -32,8 +33,13 @@ const flushService = async (opts, databaseId) => {
|
|
|
32
33
|
}
|
|
33
34
|
});
|
|
34
35
|
}
|
|
36
|
+
const routingSchema = (0, routing_1.getRoutingSchema)(opts);
|
|
37
|
+
if (!(0, routing_1.isValidSchemaName)(routingSchema)) {
|
|
38
|
+
log.warn(`[flush] invalid routing schema name: ${routingSchema}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
35
41
|
const svc = await pgPool.query(`SELECT hostname
|
|
36
|
-
FROM
|
|
42
|
+
FROM "${routingSchema}".domains
|
|
37
43
|
WHERE database_id = $1`, [databaseId]);
|
|
38
44
|
if (svc.rowCount === 0)
|
|
39
45
|
return;
|
package/middleware/routing.d.ts
CHANGED
|
@@ -19,6 +19,15 @@ export interface ResolvedRoute {
|
|
|
19
19
|
tls_status: string | null;
|
|
20
20
|
tls_secret_name: string | null;
|
|
21
21
|
}
|
|
22
|
+
/** Published logical name of the database-scope routing plane. */
|
|
23
|
+
export declare const DEFAULT_ROUTING_SCHEMA = "routing_public";
|
|
24
|
+
/** The routing-plane schema in effect: configured override or the published default. */
|
|
25
|
+
export declare const getRoutingSchema: (opts: {
|
|
26
|
+
api?: {
|
|
27
|
+
routingSchema?: string;
|
|
28
|
+
};
|
|
29
|
+
}) => string;
|
|
30
|
+
export declare const isValidSchemaName: (name: string) => boolean;
|
|
22
31
|
/**
|
|
23
32
|
* Resolve a hostname through the compiled scoped-routing plane (host-only:
|
|
24
33
|
* path/method routing belongs to Traefik/Ingress, not the server).
|
package/middleware/routing.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.routeToApiStructure = exports.resolveRoute = void 0;
|
|
3
|
+
exports.routeToApiStructure = exports.resolveRoute = exports.isValidSchemaName = exports.getRoutingSchema = exports.DEFAULT_ROUTING_SCHEMA = void 0;
|
|
4
4
|
const logger_1 = require("@pgpmjs/logger");
|
|
5
5
|
const log = new logger_1.Logger('routing');
|
|
6
6
|
const RESOLVER_FUNCTION = 'resolve_route';
|
|
7
|
+
/** Published logical name of the database-scope routing plane. */
|
|
8
|
+
exports.DEFAULT_ROUTING_SCHEMA = 'routing_public';
|
|
9
|
+
/** The routing-plane schema in effect: configured override or the published default. */
|
|
10
|
+
const getRoutingSchema = (opts) => opts.api?.routingSchema || exports.DEFAULT_ROUTING_SCHEMA;
|
|
11
|
+
exports.getRoutingSchema = getRoutingSchema;
|
|
7
12
|
const isValidSchemaName = (name) => /^[a-z_][a-z0-9_]*$/.test(name);
|
|
13
|
+
exports.isValidSchemaName = isValidSchemaName;
|
|
8
14
|
/**
|
|
9
15
|
* Resolve a hostname through the compiled scoped-routing plane (host-only:
|
|
10
16
|
* path/method routing belongs to Traefik/Ingress, not the server).
|
|
@@ -13,7 +19,7 @@ const isValidSchemaName = (name) => /^[a-z_][a-z0-9_]*$/.test(name);
|
|
|
13
19
|
* treats it as a hard no-match (→ 404); there is no legacy fallback.
|
|
14
20
|
*/
|
|
15
21
|
const resolveRoute = async (pool, schema, host) => {
|
|
16
|
-
if (!isValidSchemaName(schema)) {
|
|
22
|
+
if (!(0, exports.isValidSchemaName)(schema)) {
|
|
17
23
|
log.warn(`[resolve-route] invalid routing schema name: ${schema}`);
|
|
18
24
|
return null;
|
|
19
25
|
}
|
|
@@ -58,7 +64,6 @@ const routeToApiStructure = (route, opts) => {
|
|
|
58
64
|
anonRole: config.anon_role || 'anon',
|
|
59
65
|
roleName: config.role_name || 'authenticated',
|
|
60
66
|
schema: config.schemas,
|
|
61
|
-
apiModules: [],
|
|
62
67
|
domains: [],
|
|
63
68
|
databaseId: config.database_id,
|
|
64
69
|
isPublic: config.is_public ?? (opts.api?.isPublic ?? false)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@constructive-io/csrf": "^0.18.2",
|
|
45
|
-
"@constructive-io/express-context": "^0.
|
|
46
|
-
"@constructive-io/graphql-env": "^3.
|
|
47
|
-
"@constructive-io/graphql-types": "^3.
|
|
45
|
+
"@constructive-io/express-context": "^0.15.0",
|
|
46
|
+
"@constructive-io/graphql-env": "^3.20.0",
|
|
47
|
+
"@constructive-io/graphql-types": "^3.19.0",
|
|
48
48
|
"@constructive-io/query-builder": "^3.1.2",
|
|
49
49
|
"@constructive-io/s3-utils": "^2.21.2",
|
|
50
50
|
"@constructive-io/url-domains": "^2.20.2",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@pgpmjs/logger": "^2.15.2",
|
|
54
54
|
"@pgpmjs/server-utils": "^3.16.2",
|
|
55
55
|
"@pgpmjs/types": "^2.37.2",
|
|
56
|
-
"agentic-server": "0.
|
|
56
|
+
"agentic-server": "0.13.0",
|
|
57
57
|
"cors": "^2.8.6",
|
|
58
58
|
"deepmerge": "^4.3.1",
|
|
59
59
|
"express": "^5.2.1",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"graphile-build-pg": "5.0.2",
|
|
65
65
|
"graphile-cache": "^4.1.2",
|
|
66
66
|
"graphile-config": "1.0.1",
|
|
67
|
-
"graphile-function-bindings": "^1.
|
|
68
|
-
"graphile-settings": "^6.
|
|
67
|
+
"graphile-function-bindings": "^1.2.0",
|
|
68
|
+
"graphile-settings": "^6.2.0",
|
|
69
69
|
"graphile-utils": "5.0.1",
|
|
70
70
|
"graphql": "16.13.0",
|
|
71
71
|
"graphql-upload": "^13.0.0",
|
|
@@ -87,10 +87,10 @@
|
|
|
87
87
|
"@types/pg": "^8.20.0",
|
|
88
88
|
"@types/request-ip": "^0.0.41",
|
|
89
89
|
"cookie-parser": "^1.4.7",
|
|
90
|
-
"graphile-test": "5.
|
|
90
|
+
"graphile-test": "5.2.0",
|
|
91
91
|
"makage": "^0.3.0",
|
|
92
92
|
"nodemon": "^3.1.14",
|
|
93
93
|
"ts-node": "^10.9.2"
|
|
94
94
|
},
|
|
95
|
-
"gitHead": "
|
|
95
|
+
"gitHead": "518e7ab2270661c64875258436d90fdf76b4b336"
|
|
96
96
|
}
|
package/server.js
CHANGED
|
@@ -36,6 +36,7 @@ const debug_db_1 = require("./middleware/observability/debug-db");
|
|
|
36
36
|
const debug_memory_1 = require("./middleware/observability/debug-memory");
|
|
37
37
|
const guard_1 = require("./middleware/observability/guard");
|
|
38
38
|
const request_logger_1 = require("./middleware/observability/request-logger");
|
|
39
|
+
const routing_1 = require("./middleware/routing");
|
|
39
40
|
const log = new logger_1.Logger('server');
|
|
40
41
|
/**
|
|
41
42
|
* Creates and starts a GraphQL server instance
|
|
@@ -92,7 +93,7 @@ class Server {
|
|
|
92
93
|
serverHost: effectiveOpts.server?.host,
|
|
93
94
|
serverPort: effectiveOpts.server?.port,
|
|
94
95
|
apiIsPublic: apiOpts.isPublic,
|
|
95
|
-
|
|
96
|
+
routingSchema: apiOpts.routingSchema,
|
|
96
97
|
metaSchemas: apiOpts.metaSchemas?.join(',') || 'default',
|
|
97
98
|
observabilityEnabled
|
|
98
99
|
});
|
|
@@ -143,7 +144,11 @@ class Server {
|
|
|
143
144
|
app.use(requestLogger);
|
|
144
145
|
app.use(api);
|
|
145
146
|
app.use(authenticate);
|
|
146
|
-
app.use((0, express_context_1.createContextMiddleware)({
|
|
147
|
+
app.use((0, express_context_1.createContextMiddleware)({
|
|
148
|
+
pg: effectiveOpts.pg,
|
|
149
|
+
loaders: (0, express_context_1.createDefaultRegistry)(),
|
|
150
|
+
routingSchema: (0, routing_1.getRoutingSchema)(effectiveOpts)
|
|
151
|
+
}));
|
|
147
152
|
app.use((0, captcha_1.createCaptchaMiddleware)());
|
|
148
153
|
// CSRF protection for cookie-authenticated requests
|
|
149
154
|
// Skip CSRF for Bearer token auth (not vulnerable to CSRF) and anonymous requests
|
package/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PgpmOptions } from '@pgpmjs/types';
|
|
2
2
|
import type { ApiOptions as ApiConfig } from '@constructive-io/graphql-types';
|
|
3
|
-
export type { ApiConfigResult, ApiError,
|
|
3
|
+
export type { ApiConfigResult, ApiError, ApiStructure, AuthSettings, DatabaseSettings, PubkeyChallengeSettings, RlsModule, WebauthnSettings, } from '@constructive-io/express-context';
|
|
4
4
|
export type ApiOptions = PgpmOptions & {
|
|
5
5
|
api?: ApiConfig;
|
|
6
6
|
};
|