@constructive-io/graphql-server 5.33.4 → 5.34.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/middleware/api.js +12 -19
- package/esm/middleware/auth.js +1 -5
- package/esm/middleware/graphile.js +14 -16
- package/esm/middleware/observability/request-logger.js +2 -2
- package/esm/middleware/routing.js +4 -3
- package/middleware/api.js +11 -18
- package/middleware/auth.js +1 -5
- package/middleware/graphile.js +14 -16
- package/middleware/observability/request-logger.js +2 -2
- package/middleware/routing.d.ts +2 -0
- package/middleware/routing.js +6 -4
- package/package.json +5 -4
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 { requireApiRole, resolveRouteAcrossPlanes, routeToApiStructure } from './routing';
|
|
9
|
+
import { requireApiRole, resolveRouteAcrossPlanes, routeToApiStructure, servedDatabasePool } from './routing';
|
|
10
10
|
const log = new Logger('api');
|
|
11
11
|
// =============================================================================
|
|
12
12
|
// Module Loader Registry (replaces inline SQL queries for per-db config)
|
|
@@ -23,7 +23,7 @@ const defaultRegistry = createDefaultRegistry();
|
|
|
23
23
|
// `is_published` governs cross-scope route visibility, not internal
|
|
24
24
|
// addressability, so it does not filter this lookup.
|
|
25
25
|
const API_BY_NAME_SQL = `
|
|
26
|
-
SELECT api_id, api_database_id AS database_id,
|
|
26
|
+
SELECT api_id, api_database_id AS database_id, role_name, anon_role, is_public, schemas
|
|
27
27
|
FROM function_resolution.api_by_name($1, $2)
|
|
28
28
|
LIMIT 1
|
|
29
29
|
`;
|
|
@@ -31,18 +31,12 @@ const API_BY_NAME_SQL = `
|
|
|
31
31
|
* Build a LoaderContext from the API row and options.
|
|
32
32
|
* This is used to resolve per-database module settings via the loader registry.
|
|
33
33
|
*/
|
|
34
|
-
const buildLoaderContext = (routingPool, opts, row) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
tenantPool: getPgPool({ ...opts.pg, database: dbname }),
|
|
41
|
-
databaseId: row.database_id,
|
|
42
|
-
apiId: row.api_id,
|
|
43
|
-
dbname
|
|
44
|
-
};
|
|
45
|
-
};
|
|
34
|
+
const buildLoaderContext = (routingPool, opts, row) => ({
|
|
35
|
+
routingPool,
|
|
36
|
+
tenantPool: servedDatabasePool(opts),
|
|
37
|
+
databaseId: row.database_id,
|
|
38
|
+
apiId: row.api_id
|
|
39
|
+
});
|
|
46
40
|
/**
|
|
47
41
|
* Resolve all per-database module settings in parallel via the loader registry.
|
|
48
42
|
* Each loader independently caches by databaseId — repeated calls are cheap.
|
|
@@ -129,7 +123,7 @@ export const getSvcKey = (opts, req) => {
|
|
|
129
123
|
};
|
|
130
124
|
const toApiStructure = (row, opts, settings = {}) => ({
|
|
131
125
|
apiId: row.api_id,
|
|
132
|
-
|
|
126
|
+
tenantPool: servedDatabasePool(opts),
|
|
133
127
|
anonRole: requireApiRole('anon_role', row.anon_role, row.api_id),
|
|
134
128
|
roleName: requireApiRole('role_name', row.role_name, row.api_id),
|
|
135
129
|
schema: row.schemas || [],
|
|
@@ -144,7 +138,7 @@ const toApiStructure = (row, opts, settings = {}) => ({
|
|
|
144
138
|
webauthnSettings: settings.webauthnSettings
|
|
145
139
|
});
|
|
146
140
|
const createAdminStructure = (opts, schemas, databaseId) => ({
|
|
147
|
-
|
|
141
|
+
tenantPool: servedDatabasePool(opts),
|
|
148
142
|
anonRole: 'anonymous',
|
|
149
143
|
roleName: 'authenticated',
|
|
150
144
|
schema: schemas,
|
|
@@ -221,13 +215,12 @@ const resolveScopedRoute = async (ctx) => {
|
|
|
221
215
|
error.code = 'CROSS_DATABASE_ROUTE';
|
|
222
216
|
throw error;
|
|
223
217
|
}
|
|
224
|
-
log.debug(`[scoped-routing] resolved host=${host} → api=${structure.apiId}
|
|
218
|
+
log.debug(`[scoped-routing] resolved host=${host} → api=${structure.apiId} databaseId=${structure.databaseId}`);
|
|
225
219
|
if (!structure.databaseId || !structure.apiId)
|
|
226
220
|
return structure;
|
|
227
221
|
const loaderCtx = buildLoaderContext(pool, opts, {
|
|
228
222
|
api_id: structure.apiId,
|
|
229
223
|
database_id: structure.databaseId,
|
|
230
|
-
dbname: structure.dbname,
|
|
231
224
|
role_name: structure.roleName,
|
|
232
225
|
anon_role: structure.anonRole,
|
|
233
226
|
is_public: structure.isPublic ?? false,
|
|
@@ -324,7 +317,7 @@ export const createApiMiddleware = (opts) => {
|
|
|
324
317
|
}
|
|
325
318
|
req.api = apiConfig;
|
|
326
319
|
req.databaseId = apiConfig.databaseId;
|
|
327
|
-
log.debug(`Resolved API:
|
|
320
|
+
log.debug(`Resolved API: databaseId=${apiConfig.databaseId}, schemas=[${apiConfig.schema?.join(', ')}]`);
|
|
328
321
|
next();
|
|
329
322
|
}
|
|
330
323
|
catch (error) {
|
package/esm/middleware/auth.js
CHANGED
|
@@ -2,7 +2,6 @@ import './types'; // for Request type
|
|
|
2
2
|
import { errors } from '@constructive-io/errors';
|
|
3
3
|
import { getNodeEnv } from '@pgpmjs/env';
|
|
4
4
|
import { Logger } from '@pgpmjs/logger';
|
|
5
|
-
import { getPgPool } from 'pg-cache';
|
|
6
5
|
import pgQueryContext from 'pg-query-context';
|
|
7
6
|
import { respondWithGraphQLError } from '../errors/graphql-response';
|
|
8
7
|
const log = new Logger('auth');
|
|
@@ -38,10 +37,7 @@ export const createAuthenticateMiddleware = (opts) => {
|
|
|
38
37
|
res.status(500).send('Missing API info');
|
|
39
38
|
return;
|
|
40
39
|
}
|
|
41
|
-
const pool =
|
|
42
|
-
...opts.pg,
|
|
43
|
-
database: api.dbname,
|
|
44
|
-
});
|
|
40
|
+
const pool = api.tenantPool;
|
|
45
41
|
const rlsModule = api.rlsModule;
|
|
46
42
|
log.info(`[auth] rlsModule=${rlsModule ? 'present' : 'missing'}, ` +
|
|
47
43
|
`authenticate=${rlsModule?.authenticate ?? 'none'}, ` +
|
|
@@ -4,9 +4,8 @@ import { DEFAULT_REQUEST_PROTECTION, protectionPgSettings } from '@constructive-
|
|
|
4
4
|
import { Logger } from '@pgpmjs/logger';
|
|
5
5
|
import { cacheGraphileInstance, createGraphileInstance, graphileCache } from 'graphile-cache';
|
|
6
6
|
import { createFunctionBindingsPlugin } from 'graphile-function-bindings';
|
|
7
|
+
import { FunctionFromSourcePreset } from 'graphile-function-source';
|
|
7
8
|
import { createConstructivePreset, createGrafastCacheLimitsPreset } from 'graphile-settings';
|
|
8
|
-
import { getPgPool } from 'pg-cache';
|
|
9
|
-
import { getPgEnvOptions } from 'pg-env';
|
|
10
9
|
import { isGraphqlObservabilityEnabled } from '../diagnostics/observability';
|
|
11
10
|
import { HandlerCreationError } from '../errors/api-errors';
|
|
12
11
|
import { respondWithGraphQLError } from '../errors/graphql-response';
|
|
@@ -56,7 +55,7 @@ const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
|
|
|
56
55
|
* plugin preset. Without settings the default preset is used
|
|
57
56
|
* (everything on except aggregates).
|
|
58
57
|
*/
|
|
59
|
-
const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn) => {
|
|
58
|
+
const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn, platform) => {
|
|
60
59
|
const introspection = makeIntrospectionWiring(pool, schemas, graphileOptions, introspectionRole);
|
|
61
60
|
const configuredPreset = graphileOptions?.preset ?? {};
|
|
62
61
|
const grafastCachePreset = createGrafastCacheLimitsPreset(graphileOptions?.grafastCache);
|
|
@@ -64,6 +63,9 @@ const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, graph
|
|
|
64
63
|
...configuredPreset,
|
|
65
64
|
extends: [
|
|
66
65
|
createConstructivePreset(databaseSettings),
|
|
66
|
+
// Only on the platform database's own APIs: a tenant never authors
|
|
67
|
+
// functions through these mutations.
|
|
68
|
+
...(platform ? [FunctionFromSourcePreset] : []),
|
|
67
69
|
...(graphileOptions?.extends ?? []),
|
|
68
70
|
...(configuredPreset.extends ?? []),
|
|
69
71
|
...introspection.presets,
|
|
@@ -261,17 +263,18 @@ export const graphile = (opts) => {
|
|
|
261
263
|
respondWithGraphQLError(res, errors.INTERNAL_FAILURE({ details: 'Missing service cache key' }));
|
|
262
264
|
return;
|
|
263
265
|
}
|
|
264
|
-
const {
|
|
266
|
+
const { anonRole, roleName, schema } = api;
|
|
267
|
+
const databaseLabel = api.databaseId ?? 'none';
|
|
265
268
|
const schemaLabel = schema?.join(',') || 'unknown';
|
|
266
269
|
// =========================================================================
|
|
267
270
|
// Phase A: Cache Check (fast path)
|
|
268
271
|
// =========================================================================
|
|
269
272
|
const cached = graphileCache.get(key);
|
|
270
273
|
if (cached) {
|
|
271
|
-
log.debug(`${label} PostGraphile cache hit key=${key}
|
|
274
|
+
log.debug(`${label} PostGraphile cache hit key=${key} databaseId=${databaseLabel} schemas=${schemaLabel}`);
|
|
272
275
|
return cached.handler(req, res, next);
|
|
273
276
|
}
|
|
274
|
-
log.debug(`${label} PostGraphile cache miss key=${key}
|
|
277
|
+
log.debug(`${label} PostGraphile cache miss key=${key} databaseId=${databaseLabel} schemas=${schemaLabel}`);
|
|
275
278
|
// =========================================================================
|
|
276
279
|
// Phase B: In-Flight Check (single-flight coalescing)
|
|
277
280
|
// =========================================================================
|
|
@@ -303,19 +306,14 @@ export const graphile = (opts) => {
|
|
|
303
306
|
const retryInstance = await retryInFlight;
|
|
304
307
|
return retryInstance.handler(req, res, next);
|
|
305
308
|
}
|
|
306
|
-
log.info(`${label} Building PostGraphile v5 handler key=${key}
|
|
307
|
-
const
|
|
308
|
-
...opts.pg,
|
|
309
|
-
database: dbname
|
|
310
|
-
});
|
|
311
|
-
// Route through pg-cache so the pool is tracked and can be cleaned up
|
|
312
|
-
// properly, preventing leaked connections during database teardown.
|
|
313
|
-
const pool = getPgPool(pgConfig);
|
|
309
|
+
log.info(`${label} Building PostGraphile v5 handler key=${key} databaseId=${databaseLabel} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
|
|
310
|
+
const pool = api.tenantPool;
|
|
314
311
|
// Register the creation promise before any asynchronous preset work so
|
|
315
312
|
// concurrent requests coalesce while optional plugin modules load.
|
|
316
313
|
const creationPromise = (async () => {
|
|
317
314
|
const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
|
|
318
|
-
const
|
|
315
|
+
const platform = api.databaseId ? await req.constructive?.useModule('platform') : undefined;
|
|
316
|
+
const preset = buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings), platform?.platform);
|
|
319
317
|
const instance = await observeGraphileBuild({
|
|
320
318
|
cacheKey: key,
|
|
321
319
|
serviceKey: key,
|
|
@@ -333,7 +331,7 @@ export const graphile = (opts) => {
|
|
|
333
331
|
creating.set(key, creationPromise);
|
|
334
332
|
try {
|
|
335
333
|
const instance = await creationPromise;
|
|
336
|
-
log.info(`${label} Cached PostGraphile v5 handler key=${key}
|
|
334
|
+
log.info(`${label} Cached PostGraphile v5 handler key=${key} databaseId=${databaseLabel}`);
|
|
337
335
|
return instance.handler(req, res, next);
|
|
338
336
|
}
|
|
339
337
|
catch (error) {
|
|
@@ -18,7 +18,7 @@ export const createRequestLogger = ({ observabilityEnabled }) => {
|
|
|
18
18
|
finished = true;
|
|
19
19
|
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
20
20
|
const apiInfo = req.api
|
|
21
|
-
? `
|
|
21
|
+
? `databaseId=${req.api.databaseId ?? 'none'} schemas=${req.api.schema?.join(',') || 'none'}`
|
|
22
22
|
: 'api=unresolved';
|
|
23
23
|
const authInfo = req.token ? 'auth=token' : 'auth=anon';
|
|
24
24
|
const svcInfo = req.svc_key ? `svc=${req.svc_key}` : 'svc=unset';
|
|
@@ -31,7 +31,7 @@ export const createRequestLogger = ({ observabilityEnabled }) => {
|
|
|
31
31
|
}
|
|
32
32
|
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
33
33
|
const apiInfo = req.api
|
|
34
|
-
? `
|
|
34
|
+
? `databaseId=${req.api.databaseId ?? 'none'} schemas=${req.api.schema?.join(',') || 'none'}`
|
|
35
35
|
: 'api=unresolved';
|
|
36
36
|
log.warn(`[${reqId}] connection closed before response completed ` +
|
|
37
37
|
`${req.method} ${req.originalUrl} (${durationMs.toFixed(1)} ms) ${apiInfo}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Logger } from '@pgpmjs/logger';
|
|
2
2
|
import { QuoteUtils } from '@pgsql/quotes';
|
|
3
|
+
import { getPgPool } from 'pg-cache';
|
|
3
4
|
const log = new Logger('routing');
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// Scoped routing plane (resolve_route contract)
|
|
@@ -141,6 +142,8 @@ export const resolveRouteAcrossPlanes = async (pool, host) => {
|
|
|
141
142
|
* rest of the middleware chain. Returns null when the route target is not an
|
|
142
143
|
* api surface or its resolved_config lacks the api essentials (→ 404).
|
|
143
144
|
*/
|
|
145
|
+
/** The server always serves its own configured database. */
|
|
146
|
+
export const servedDatabasePool = (opts) => getPgPool({ ...opts.pg });
|
|
144
147
|
export const routeToApiStructure = (route, opts) => {
|
|
145
148
|
if (route.target_module !== 'apis' && route.target_module !== 'api') {
|
|
146
149
|
return null;
|
|
@@ -153,9 +156,7 @@ export const routeToApiStructure = (route, opts) => {
|
|
|
153
156
|
const apiId = config.api_id ?? route.target_source_id ?? undefined;
|
|
154
157
|
return {
|
|
155
158
|
apiId,
|
|
156
|
-
|
|
157
|
-
// database; fall back to the server's own database in that case.
|
|
158
|
-
dbname: config.dbname || opts.pg?.database || '',
|
|
159
|
+
tenantPool: servedDatabasePool(opts),
|
|
159
160
|
anonRole: requireApiRole('anon_role', config.anon_role, apiId),
|
|
160
161
|
roleName: requireApiRole('role_name', config.role_name, apiId),
|
|
161
162
|
schema: config.schemas,
|
package/middleware/api.js
CHANGED
|
@@ -29,7 +29,7 @@ const defaultRegistry = (0, express_context_1.createDefaultRegistry)();
|
|
|
29
29
|
// `is_published` governs cross-scope route visibility, not internal
|
|
30
30
|
// addressability, so it does not filter this lookup.
|
|
31
31
|
const API_BY_NAME_SQL = `
|
|
32
|
-
SELECT api_id, api_database_id AS database_id,
|
|
32
|
+
SELECT api_id, api_database_id AS database_id, role_name, anon_role, is_public, schemas
|
|
33
33
|
FROM function_resolution.api_by_name($1, $2)
|
|
34
34
|
LIMIT 1
|
|
35
35
|
`;
|
|
@@ -37,18 +37,12 @@ const API_BY_NAME_SQL = `
|
|
|
37
37
|
* Build a LoaderContext from the API row and options.
|
|
38
38
|
* This is used to resolve per-database module settings via the loader registry.
|
|
39
39
|
*/
|
|
40
|
-
const buildLoaderContext = (routingPool, opts, row) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
tenantPool: (0, pg_cache_1.getPgPool)({ ...opts.pg, database: dbname }),
|
|
47
|
-
databaseId: row.database_id,
|
|
48
|
-
apiId: row.api_id,
|
|
49
|
-
dbname
|
|
50
|
-
};
|
|
51
|
-
};
|
|
40
|
+
const buildLoaderContext = (routingPool, opts, row) => ({
|
|
41
|
+
routingPool,
|
|
42
|
+
tenantPool: (0, routing_1.servedDatabasePool)(opts),
|
|
43
|
+
databaseId: row.database_id,
|
|
44
|
+
apiId: row.api_id
|
|
45
|
+
});
|
|
52
46
|
/**
|
|
53
47
|
* Resolve all per-database module settings in parallel via the loader registry.
|
|
54
48
|
* Each loader independently caches by databaseId — repeated calls are cheap.
|
|
@@ -137,7 +131,7 @@ const getSvcKey = (opts, req) => {
|
|
|
137
131
|
exports.getSvcKey = getSvcKey;
|
|
138
132
|
const toApiStructure = (row, opts, settings = {}) => ({
|
|
139
133
|
apiId: row.api_id,
|
|
140
|
-
|
|
134
|
+
tenantPool: (0, routing_1.servedDatabasePool)(opts),
|
|
141
135
|
anonRole: (0, routing_1.requireApiRole)('anon_role', row.anon_role, row.api_id),
|
|
142
136
|
roleName: (0, routing_1.requireApiRole)('role_name', row.role_name, row.api_id),
|
|
143
137
|
schema: row.schemas || [],
|
|
@@ -152,7 +146,7 @@ const toApiStructure = (row, opts, settings = {}) => ({
|
|
|
152
146
|
webauthnSettings: settings.webauthnSettings
|
|
153
147
|
});
|
|
154
148
|
const createAdminStructure = (opts, schemas, databaseId) => ({
|
|
155
|
-
|
|
149
|
+
tenantPool: (0, routing_1.servedDatabasePool)(opts),
|
|
156
150
|
anonRole: 'anonymous',
|
|
157
151
|
roleName: 'authenticated',
|
|
158
152
|
schema: schemas,
|
|
@@ -229,13 +223,12 @@ const resolveScopedRoute = async (ctx) => {
|
|
|
229
223
|
error.code = 'CROSS_DATABASE_ROUTE';
|
|
230
224
|
throw error;
|
|
231
225
|
}
|
|
232
|
-
log.debug(`[scoped-routing] resolved host=${host} → api=${structure.apiId}
|
|
226
|
+
log.debug(`[scoped-routing] resolved host=${host} → api=${structure.apiId} databaseId=${structure.databaseId}`);
|
|
233
227
|
if (!structure.databaseId || !structure.apiId)
|
|
234
228
|
return structure;
|
|
235
229
|
const loaderCtx = buildLoaderContext(pool, opts, {
|
|
236
230
|
api_id: structure.apiId,
|
|
237
231
|
database_id: structure.databaseId,
|
|
238
|
-
dbname: structure.dbname,
|
|
239
232
|
role_name: structure.roleName,
|
|
240
233
|
anon_role: structure.anonRole,
|
|
241
234
|
is_public: structure.isPublic ?? false,
|
|
@@ -333,7 +326,7 @@ const createApiMiddleware = (opts) => {
|
|
|
333
326
|
}
|
|
334
327
|
req.api = apiConfig;
|
|
335
328
|
req.databaseId = apiConfig.databaseId;
|
|
336
|
-
log.debug(`Resolved API:
|
|
329
|
+
log.debug(`Resolved API: databaseId=${apiConfig.databaseId}, schemas=[${apiConfig.schema?.join(', ')}]`);
|
|
337
330
|
next();
|
|
338
331
|
}
|
|
339
332
|
catch (error) {
|
package/middleware/auth.js
CHANGED
|
@@ -8,7 +8,6 @@ require("./types"); // for Request type
|
|
|
8
8
|
const errors_1 = require("@constructive-io/errors");
|
|
9
9
|
const env_1 = require("@pgpmjs/env");
|
|
10
10
|
const logger_1 = require("@pgpmjs/logger");
|
|
11
|
-
const pg_cache_1 = require("pg-cache");
|
|
12
11
|
const pg_query_context_1 = __importDefault(require("pg-query-context"));
|
|
13
12
|
const graphql_response_1 = require("../errors/graphql-response");
|
|
14
13
|
const log = new logger_1.Logger('auth');
|
|
@@ -45,10 +44,7 @@ const createAuthenticateMiddleware = (opts) => {
|
|
|
45
44
|
res.status(500).send('Missing API info');
|
|
46
45
|
return;
|
|
47
46
|
}
|
|
48
|
-
const pool =
|
|
49
|
-
...opts.pg,
|
|
50
|
-
database: api.dbname,
|
|
51
|
-
});
|
|
47
|
+
const pool = api.tenantPool;
|
|
52
48
|
const rlsModule = api.rlsModule;
|
|
53
49
|
log.info(`[auth] rlsModule=${rlsModule ? 'present' : 'missing'}, ` +
|
|
54
50
|
`authenticate=${rlsModule?.authenticate ?? 'none'}, ` +
|
package/middleware/graphile.js
CHANGED
|
@@ -10,9 +10,8 @@ const express_context_1 = require("@constructive-io/express-context");
|
|
|
10
10
|
const logger_1 = require("@pgpmjs/logger");
|
|
11
11
|
const graphile_cache_1 = require("graphile-cache");
|
|
12
12
|
const graphile_function_bindings_1 = require("graphile-function-bindings");
|
|
13
|
+
const graphile_function_source_1 = require("graphile-function-source");
|
|
13
14
|
const graphile_settings_1 = require("graphile-settings");
|
|
14
|
-
const pg_cache_1 = require("pg-cache");
|
|
15
|
-
const pg_env_1 = require("pg-env");
|
|
16
15
|
const observability_1 = require("../diagnostics/observability");
|
|
17
16
|
const api_errors_1 = require("../errors/api-errors");
|
|
18
17
|
const graphql_response_1 = require("../errors/graphql-response");
|
|
@@ -62,7 +61,7 @@ const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
|
|
|
62
61
|
* plugin preset. Without settings the default preset is used
|
|
63
62
|
* (everything on except aggregates).
|
|
64
63
|
*/
|
|
65
|
-
const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn) => {
|
|
64
|
+
const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn, platform) => {
|
|
66
65
|
const introspection = (0, graphile_introspection_1.makeIntrospectionWiring)(pool, schemas, graphileOptions, introspectionRole);
|
|
67
66
|
const configuredPreset = graphileOptions?.preset ?? {};
|
|
68
67
|
const grafastCachePreset = (0, graphile_settings_1.createGrafastCacheLimitsPreset)(graphileOptions?.grafastCache);
|
|
@@ -70,6 +69,9 @@ const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, graph
|
|
|
70
69
|
...configuredPreset,
|
|
71
70
|
extends: [
|
|
72
71
|
(0, graphile_settings_1.createConstructivePreset)(databaseSettings),
|
|
72
|
+
// Only on the platform database's own APIs: a tenant never authors
|
|
73
|
+
// functions through these mutations.
|
|
74
|
+
...(platform ? [graphile_function_source_1.FunctionFromSourcePreset] : []),
|
|
73
75
|
...(graphileOptions?.extends ?? []),
|
|
74
76
|
...(configuredPreset.extends ?? []),
|
|
75
77
|
...introspection.presets,
|
|
@@ -267,17 +269,18 @@ const graphile = (opts) => {
|
|
|
267
269
|
(0, graphql_response_1.respondWithGraphQLError)(res, errors_1.errors.INTERNAL_FAILURE({ details: 'Missing service cache key' }));
|
|
268
270
|
return;
|
|
269
271
|
}
|
|
270
|
-
const {
|
|
272
|
+
const { anonRole, roleName, schema } = api;
|
|
273
|
+
const databaseLabel = api.databaseId ?? 'none';
|
|
271
274
|
const schemaLabel = schema?.join(',') || 'unknown';
|
|
272
275
|
// =========================================================================
|
|
273
276
|
// Phase A: Cache Check (fast path)
|
|
274
277
|
// =========================================================================
|
|
275
278
|
const cached = graphile_cache_1.graphileCache.get(key);
|
|
276
279
|
if (cached) {
|
|
277
|
-
log.debug(`${label} PostGraphile cache hit key=${key}
|
|
280
|
+
log.debug(`${label} PostGraphile cache hit key=${key} databaseId=${databaseLabel} schemas=${schemaLabel}`);
|
|
278
281
|
return cached.handler(req, res, next);
|
|
279
282
|
}
|
|
280
|
-
log.debug(`${label} PostGraphile cache miss key=${key}
|
|
283
|
+
log.debug(`${label} PostGraphile cache miss key=${key} databaseId=${databaseLabel} schemas=${schemaLabel}`);
|
|
281
284
|
// =========================================================================
|
|
282
285
|
// Phase B: In-Flight Check (single-flight coalescing)
|
|
283
286
|
// =========================================================================
|
|
@@ -309,19 +312,14 @@ const graphile = (opts) => {
|
|
|
309
312
|
const retryInstance = await retryInFlight;
|
|
310
313
|
return retryInstance.handler(req, res, next);
|
|
311
314
|
}
|
|
312
|
-
log.info(`${label} Building PostGraphile v5 handler key=${key}
|
|
313
|
-
const
|
|
314
|
-
...opts.pg,
|
|
315
|
-
database: dbname
|
|
316
|
-
});
|
|
317
|
-
// Route through pg-cache so the pool is tracked and can be cleaned up
|
|
318
|
-
// properly, preventing leaked connections during database teardown.
|
|
319
|
-
const pool = (0, pg_cache_1.getPgPool)(pgConfig);
|
|
315
|
+
log.info(`${label} Building PostGraphile v5 handler key=${key} databaseId=${databaseLabel} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
|
|
316
|
+
const pool = api.tenantPool;
|
|
320
317
|
// Register the creation promise before any asynchronous preset work so
|
|
321
318
|
// concurrent requests coalesce while optional plugin modules load.
|
|
322
319
|
const creationPromise = (async () => {
|
|
323
320
|
const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
|
|
324
|
-
const
|
|
321
|
+
const platform = api.databaseId ? await req.constructive?.useModule('platform') : undefined;
|
|
322
|
+
const preset = buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings), platform?.platform);
|
|
325
323
|
const instance = await (0, graphile_build_stats_1.observeGraphileBuild)({
|
|
326
324
|
cacheKey: key,
|
|
327
325
|
serviceKey: key,
|
|
@@ -339,7 +337,7 @@ const graphile = (opts) => {
|
|
|
339
337
|
creating.set(key, creationPromise);
|
|
340
338
|
try {
|
|
341
339
|
const instance = await creationPromise;
|
|
342
|
-
log.info(`${label} Cached PostGraphile v5 handler key=${key}
|
|
340
|
+
log.info(`${label} Cached PostGraphile v5 handler key=${key} databaseId=${databaseLabel}`);
|
|
343
341
|
return instance.handler(req, res, next);
|
|
344
342
|
}
|
|
345
343
|
catch (error) {
|
|
@@ -21,7 +21,7 @@ const createRequestLogger = ({ observabilityEnabled }) => {
|
|
|
21
21
|
finished = true;
|
|
22
22
|
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
23
23
|
const apiInfo = req.api
|
|
24
|
-
? `
|
|
24
|
+
? `databaseId=${req.api.databaseId ?? 'none'} schemas=${req.api.schema?.join(',') || 'none'}`
|
|
25
25
|
: 'api=unresolved';
|
|
26
26
|
const authInfo = req.token ? 'auth=token' : 'auth=anon';
|
|
27
27
|
const svcInfo = req.svc_key ? `svc=${req.svc_key}` : 'svc=unset';
|
|
@@ -34,7 +34,7 @@ const createRequestLogger = ({ observabilityEnabled }) => {
|
|
|
34
34
|
}
|
|
35
35
|
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
36
36
|
const apiInfo = req.api
|
|
37
|
-
? `
|
|
37
|
+
? `databaseId=${req.api.databaseId ?? 'none'} schemas=${req.api.schema?.join(',') || 'none'}`
|
|
38
38
|
: 'api=unresolved';
|
|
39
39
|
log.warn(`[${reqId}] connection closed before response completed ` +
|
|
40
40
|
`${req.method} ${req.originalUrl} (${durationMs.toFixed(1)} ms) ${apiInfo}`);
|
package/middleware/routing.d.ts
CHANGED
|
@@ -75,4 +75,6 @@ export declare const resolveRouteAcrossPlanes: (pool: Pool, host: string) => Pro
|
|
|
75
75
|
* rest of the middleware chain. Returns null when the route target is not an
|
|
76
76
|
* api surface or its resolved_config lacks the api essentials (→ 404).
|
|
77
77
|
*/
|
|
78
|
+
/** The server always serves its own configured database. */
|
|
79
|
+
export declare const servedDatabasePool: (opts: ApiOptions) => Pool;
|
|
78
80
|
export declare const routeToApiStructure: (route: ResolvedRoute, opts: ApiOptions) => ApiStructure | null;
|
package/middleware/routing.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.routeToApiStructure = exports.resolveRouteAcrossPlanes = exports.loadRoutingPlanes = exports.resolveRoute = exports.isValidSchemaName = exports.ROUTING_SCHEMA = exports.requireApiRole = void 0;
|
|
3
|
+
exports.routeToApiStructure = exports.servedDatabasePool = 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
|
+
const pg_cache_1 = require("pg-cache");
|
|
6
7
|
const log = new logger_1.Logger('routing');
|
|
7
8
|
// =============================================================================
|
|
8
9
|
// Scoped routing plane (resolve_route contract)
|
|
@@ -149,6 +150,9 @@ exports.resolveRouteAcrossPlanes = resolveRouteAcrossPlanes;
|
|
|
149
150
|
* rest of the middleware chain. Returns null when the route target is not an
|
|
150
151
|
* api surface or its resolved_config lacks the api essentials (→ 404).
|
|
151
152
|
*/
|
|
153
|
+
/** The server always serves its own configured database. */
|
|
154
|
+
const servedDatabasePool = (opts) => (0, pg_cache_1.getPgPool)({ ...opts.pg });
|
|
155
|
+
exports.servedDatabasePool = servedDatabasePool;
|
|
152
156
|
const routeToApiStructure = (route, opts) => {
|
|
153
157
|
if (route.target_module !== 'apis' && route.target_module !== 'api') {
|
|
154
158
|
return null;
|
|
@@ -161,9 +165,7 @@ const routeToApiStructure = (route, opts) => {
|
|
|
161
165
|
const apiId = config.api_id ?? route.target_source_id ?? undefined;
|
|
162
166
|
return {
|
|
163
167
|
apiId,
|
|
164
|
-
|
|
165
|
-
// database; fall back to the server's own database in that case.
|
|
166
|
-
dbname: config.dbname || opts.pg?.database || '',
|
|
168
|
+
tenantPool: (0, exports.servedDatabasePool)(opts),
|
|
167
169
|
anonRole: (0, exports.requireApiRole)('anon_role', config.anon_role, apiId),
|
|
168
170
|
roleName: (0, exports.requireApiRole)('role_name', config.role_name, apiId),
|
|
169
171
|
schema: config.schemas,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.34.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@agentic-kit/ollama": "2.16.0",
|
|
44
44
|
"@constructive-io/csrf": "^0.31.0",
|
|
45
45
|
"@constructive-io/errors": "^0.25.0",
|
|
46
|
-
"@constructive-io/express-context": "^0.
|
|
46
|
+
"@constructive-io/express-context": "^0.36.0",
|
|
47
47
|
"@constructive-io/graphql-env": "^3.37.2",
|
|
48
48
|
"@constructive-io/graphql-types": "^3.36.1",
|
|
49
49
|
"@constructive-io/llm-env": "^0.16.0",
|
|
@@ -69,10 +69,11 @@
|
|
|
69
69
|
"graphile-cache": "^4.16.1",
|
|
70
70
|
"graphile-config": "1.1.0",
|
|
71
71
|
"graphile-function-bindings": "^1.19.0",
|
|
72
|
+
"graphile-function-source": "^0.3.0",
|
|
72
73
|
"graphile-plugin-utils": "^1.14.1",
|
|
73
74
|
"graphile-presigned-url-plugin": "^1.23.4",
|
|
74
75
|
"graphile-scoped-introspection": "^0.6.0",
|
|
75
|
-
"graphile-settings": "^6.
|
|
76
|
+
"graphile-settings": "^6.29.0",
|
|
76
77
|
"graphile-utils": "5.0.3",
|
|
77
78
|
"graphql": "16.13.0",
|
|
78
79
|
"graphql-upload": "^13.0.0",
|
|
@@ -98,5 +99,5 @@
|
|
|
98
99
|
"supertest": "^7.2.2",
|
|
99
100
|
"ts-node": "^10.9.2"
|
|
100
101
|
},
|
|
101
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "3e55ca27382b58c3cab2b32e86d1690d0cdae8ce"
|
|
102
103
|
}
|