@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.
@@ -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, dbname, role_name, anon_role, is_public, schemas
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
- // Scoped APIs leave dbname NULL when their schemas live in the serving
36
- // database (pooled tenants); fall back to the server's own database.
37
- const dbname = row.dbname || opts.pg?.database || '';
38
- return {
39
- routingPool,
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
- dbname: row.dbname || opts.pg?.database || '',
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
- dbname: opts.pg?.database ?? '',
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} db=${structure.dbname}`);
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: db=${apiConfig.dbname}, schemas=[${apiConfig.schema?.join(', ')}]`);
320
+ log.debug(`Resolved API: databaseId=${apiConfig.databaseId}, schemas=[${apiConfig.schema?.join(', ')}]`);
328
321
  next();
329
322
  }
330
323
  catch (error) {
@@ -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 = getPgPool({
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 { dbname, anonRole, roleName, schema } = api;
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} db=${dbname} schemas=${schemaLabel}`);
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} db=${dbname} schemas=${schemaLabel}`);
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} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
307
- const pgConfig = getPgEnvOptions({
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 preset = buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings));
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} db=${dbname}`);
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
- ? `db=${req.api.dbname} schemas=${req.api.schema?.join(',') || 'none'}`
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
- ? `db=${req.api.dbname} schemas=${req.api.schema?.join(',') || 'none'}`
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
- // Scoped APIs leave dbname NULL when their schemas live in the serving
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, dbname, role_name, anon_role, is_public, schemas
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
- // Scoped APIs leave dbname NULL when their schemas live in the serving
42
- // database (pooled tenants); fall back to the server's own database.
43
- const dbname = row.dbname || opts.pg?.database || '';
44
- return {
45
- routingPool,
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
- dbname: row.dbname || opts.pg?.database || '',
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
- dbname: opts.pg?.database ?? '',
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} db=${structure.dbname}`);
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: db=${apiConfig.dbname}, schemas=[${apiConfig.schema?.join(', ')}]`);
329
+ log.debug(`Resolved API: databaseId=${apiConfig.databaseId}, schemas=[${apiConfig.schema?.join(', ')}]`);
337
330
  next();
338
331
  }
339
332
  catch (error) {
@@ -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 = (0, pg_cache_1.getPgPool)({
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'}, ` +
@@ -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 { dbname, anonRole, roleName, schema } = api;
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} db=${dbname} schemas=${schemaLabel}`);
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} db=${dbname} schemas=${schemaLabel}`);
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} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
313
- const pgConfig = (0, pg_env_1.getPgEnvOptions)({
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 preset = buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings));
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} db=${dbname}`);
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
- ? `db=${req.api.dbname} schemas=${req.api.schema?.join(',') || 'none'}`
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
- ? `db=${req.api.dbname} schemas=${req.api.schema?.join(',') || 'none'}`
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}`);
@@ -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;
@@ -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
- // Scoped APIs leave dbname NULL when their schemas live in the serving
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.33.4",
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.35.3",
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.28.3",
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": "7ce27b6ff591192fe5eb87e26bb3fb1e538d6505"
102
+ "gitHead": "3e55ca27382b58c3cab2b32e86d1690d0cdae8ce"
102
103
  }