@constructive-io/graphql-server 5.0.4 → 5.1.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.
@@ -1,13 +1,12 @@
1
- import { getNodeEnv } from '@pgpmjs/env';
1
+ import './types';
2
+ import { createDefaultRegistry } from '@constructive-io/express-context';
3
+ import { parseUrl } from '@constructive-io/url-domains';
2
4
  import { Logger } from '@pgpmjs/logger';
3
5
  import { svcCache } from '@pgpmjs/server-utils';
4
- import { parseUrl } from '@constructive-io/url-domains';
5
- import { createDefaultRegistry, } from '@constructive-io/express-context';
6
6
  import { getPgPool } from 'pg-cache';
7
7
  import errorPage50x from '../errors/50x';
8
8
  import errorPage404Message from '../errors/404-message';
9
9
  import { resolveRoute, routeToApiStructure } from './routing';
10
- import './types';
11
10
  const log = new Logger('api');
12
11
  // =============================================================================
13
12
  // Module Loader Registry (replaces inline SQL queries for per-db config)
@@ -16,87 +15,49 @@ const defaultRegistry = createDefaultRegistry();
16
15
  // =============================================================================
17
16
  // SQL Queries (API resolution only — module queries now live in loaders)
18
17
  // =============================================================================
19
- const DOMAIN_LOOKUP_SQL = `
20
- SELECT
21
- a.id as api_id,
22
- a.database_id,
23
- a.dbname,
24
- a.role_name,
25
- a.anon_role,
26
- a.is_public,
27
- COALESCE(array_agg(s.schema_name) FILTER (WHERE s.schema_name IS NOT NULL), '{}') as schemas
28
- FROM services_public.domains d
29
- JOIN services_public.apis a ON d.api_id = a.id
30
- LEFT JOIN services_public.api_schemas aps ON a.id = aps.api_id
31
- LEFT JOIN metaschema_public.schema s ON aps.schema_id = s.id
32
- WHERE d.domain = $1
33
- AND (($2::text IS NULL AND d.subdomain IS NULL) OR d.subdomain = $2)
34
- AND a.is_public = $3
35
- GROUP BY a.id, a.database_id, a.dbname, a.role_name, a.anon_role, a.is_public
36
- LIMIT 1
37
- `;
38
- const API_NAME_LOOKUP_SQL = `
18
+ // Private-header X-Api-Name lookup against the scoped routing plane.
19
+ // `is_published` is the routing-plane analog of the legacy `is_public` column.
20
+ const SCOPED_API_NAME_LOOKUP_SQL = `
39
21
  SELECT
40
22
  a.id as api_id,
41
23
  a.database_id,
42
24
  a.dbname,
43
25
  a.role_name,
44
26
  a.anon_role,
45
- a.is_public,
27
+ a.is_published as is_public,
46
28
  COALESCE(array_agg(s.schema_name) FILTER (WHERE s.schema_name IS NOT NULL), '{}') as schemas
47
- FROM services_public.apis a
48
- LEFT JOIN services_public.api_schemas aps ON a.id = aps.api_id
29
+ FROM constructive_routing_public.apis a
30
+ LEFT JOIN constructive_routing_public.api_schemas aps ON a.id = aps.api_id
49
31
  LEFT JOIN metaschema_public.schema s ON aps.schema_id = s.id
50
32
  WHERE a.database_id = $1
51
33
  AND a.name = $2
52
- AND a.is_public = $3
53
- GROUP BY a.id, a.database_id, a.dbname, a.role_name, a.anon_role, a.is_public
34
+ AND a.is_published = $3
35
+ GROUP BY a.id, a.database_id, a.dbname, a.role_name, a.anon_role, a.is_published
54
36
  LIMIT 1
55
37
  `;
56
- const API_LIST_SQL = `
57
- SELECT
58
- a.id,
59
- a.database_id,
60
- a.name,
61
- a.dbname,
62
- a.role_name,
63
- a.anon_role,
64
- a.is_public,
65
- COALESCE(
66
- json_agg(
67
- json_build_object('domain', d.domain, 'subdomain', d.subdomain)
68
- ) FILTER (WHERE d.domain IS NOT NULL),
69
- '[]'
70
- ) as domains
71
- FROM services_public.apis a
72
- LEFT JOIN services_public.domains d ON a.id = d.api_id
73
- WHERE a.is_public = $1
74
- GROUP BY a.id, a.database_id, a.name, a.dbname, a.role_name, a.anon_role, a.is_public
75
- LIMIT 100
76
- `;
77
38
  /**
78
39
  * Build a LoaderContext from the API row and options.
79
40
  * This is used to resolve per-database module settings via the loader registry.
80
41
  */
81
- const buildLoaderContext = (servicesPool, opts, row) => ({
82
- servicesPool,
42
+ const buildLoaderContext = (routingPool, opts, row) => ({
43
+ routingPool,
83
44
  tenantPool: getPgPool({ ...opts.pg, database: row.dbname }),
84
45
  databaseId: row.database_id,
85
46
  apiId: row.api_id,
86
- dbname: row.dbname,
47
+ dbname: row.dbname
87
48
  });
88
49
  /**
89
50
  * Resolve all per-database module settings in parallel via the loader registry.
90
51
  * Each loader independently caches by databaseId — repeated calls are cheap.
91
52
  */
92
53
  const resolveModuleSettings = async (registry, ctx) => {
93
- const [rlsModule, authSettings, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings,] = await Promise.all([
54
+ const [rlsModule, authSettings, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings] = await Promise.all([
94
55
  registry.resolve('rlsModule', ctx),
95
56
  registry.resolve('authSettings', ctx),
96
57
  registry.resolve('corsOrigins', ctx),
97
58
  registry.resolve('databaseSettings', ctx),
98
59
  registry.resolve('pubkeyChallengeSettings', ctx),
99
- registry.resolve('webauthnSettings', ctx),
60
+ registry.resolve('webauthnSettings', ctx)
100
61
  ]);
101
62
  return {
102
63
  rlsModule,
@@ -104,7 +65,7 @@ const resolveModuleSettings = async (registry, ctx) => {
104
65
  corsOrigins,
105
66
  databaseSettings,
106
67
  pubkeyChallengeSettings,
107
- webauthnSettings,
68
+ webauthnSettings
108
69
  };
109
70
  };
110
71
  // =============================================================================
@@ -125,14 +86,14 @@ const getRoutingHeaders = (req) => ({
125
86
  schemata: req.get('X-Schemata'),
126
87
  apiName: req.get('X-Api-Name'),
127
88
  metaSchema: req.get('X-Meta-Schema'),
128
- databaseId: req.get('X-Database-Id'),
89
+ databaseId: req.get('X-Database-Id')
129
90
  });
130
91
  const getUrlDomains = (req) => {
131
92
  const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
132
93
  const parsed = parseUrl(fullUrl);
133
94
  return {
134
95
  domain: parsed.domain ?? '',
135
- subdomains: parsed.subdomains ?? [],
96
+ subdomains: parsed.subdomains ?? []
136
97
  };
137
98
  };
138
99
  export const getSubdomain = (subdomains) => {
@@ -172,7 +133,7 @@ const toApiStructure = (row, opts, settings = {}) => ({
172
133
  corsOrigins: settings.corsOrigins,
173
134
  databaseSettings: settings.databaseSettings,
174
135
  pubkeyChallengeSettings: settings.pubkeyChallengeSettings,
175
- webauthnSettings: settings.webauthnSettings,
136
+ webauthnSettings: settings.webauthnSettings
176
137
  });
177
138
  const createAdminStructure = (opts, schemas, databaseId) => ({
178
139
  dbname: opts.pg?.database ?? '',
@@ -182,7 +143,7 @@ const createAdminStructure = (opts, schemas, databaseId) => ({
182
143
  apiModules: [],
183
144
  domains: [],
184
145
  databaseId,
185
- isPublic: false,
146
+ isPublic: false
186
147
  });
187
148
  // =============================================================================
188
149
  // Database Queries (API resolution only)
@@ -191,31 +152,25 @@ const validateSchemata = async (pool, schemas) => {
191
152
  const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemas]);
192
153
  return result.rows.map((row) => row.schema_name);
193
154
  };
194
- const queryByDomain = async (pool, domain, subdomain, isPublic) => {
195
- const result = await pool.query(DOMAIN_LOOKUP_SQL, [domain, subdomain, isPublic]);
196
- return result.rows[0] ?? null;
197
- };
198
155
  const queryByApiName = async (pool, databaseId, name, isPublic) => {
199
- const result = await pool.query(API_NAME_LOOKUP_SQL, [databaseId, name, isPublic]);
156
+ const result = await pool.query(SCOPED_API_NAME_LOOKUP_SQL, [databaseId, name, isPublic]);
200
157
  return result.rows[0] ?? null;
201
158
  };
202
- const queryApiList = async (pool, isPublic) => {
203
- const result = await pool.query(API_LIST_SQL, [isPublic]);
204
- return result.rows;
205
- };
206
159
  // =============================================================================
207
160
  // Resolution Logic
208
161
  // =============================================================================
209
162
  const determineMode = (ctx) => {
210
163
  const { opts, headers } = ctx;
211
- if (opts.api?.enableServicesApi === false)
212
- return 'services-disabled';
164
+ // Static single-tenant mode: scoped routing off — expose configured schemas
165
+ // directly with no route resolution.
166
+ if (!opts.api?.enableScopedRouting)
167
+ return 'static';
213
168
  if (opts.api?.isPublic === false) {
214
- return getPrivateHeaderMode(headers) ?? 'domain-lookup';
169
+ return getPrivateHeaderMode(headers) ?? 'scoped-route';
215
170
  }
216
- return 'domain-lookup';
171
+ return 'scoped-route';
217
172
  };
218
- const resolveServicesDisabled = (ctx) => {
173
+ const resolveStatic = (ctx) => {
219
174
  const { opts } = ctx;
220
175
  return {
221
176
  dbname: opts.pg?.database ?? '',
@@ -225,7 +180,7 @@ const resolveServicesDisabled = (ctx) => {
225
180
  apiModules: [],
226
181
  domains: [],
227
182
  databaseId: opts.api?.defaultDatabaseId,
228
- isPublic: false,
183
+ isPublic: false
229
184
  };
230
185
  };
231
186
  const resolveSchemataHeader = async (ctx, validatedSchemas) => {
@@ -257,12 +212,12 @@ const resolveMetaSchemaHeader = (ctx, validatedSchemas) => {
257
212
  return createAdminStructure(ctx.opts, validatedSchemas, ctx.headers.databaseId);
258
213
  };
259
214
  /**
260
- * Scoped routing plane resolution (additive, host-only): one indexed
261
- * resolve_route() call against the compiled hostname/route bindings.
262
- * Path/method routing belongs to Traefik/Ingress — the server only maps
263
- * host → tenant/api/db/role. Returns null (fall back to the legacy
264
- * services_public lookup) when disabled, unmatched, resolver not installed,
265
- * or the target is not an api surface.
215
+ * Scoped routing plane resolution (host-only): one indexed resolve_route()
216
+ * call against the compiled hostname/route bindings. Path/method routing
217
+ * belongs to Traefik/Ingress — the server only maps host → tenant/api/db/role.
218
+ * This is the sole host resolver. Returns null ( 404) when disabled,
219
+ * unmatched, the resolver is not installed, or the target is not an api
220
+ * surface. There is no legacy fallback.
266
221
  */
267
222
  const resolveScopedRoute = async (ctx) => {
268
223
  const { opts, pool, host } = ctx;
@@ -285,7 +240,7 @@ const resolveScopedRoute = async (ctx) => {
285
240
  role_name: structure.roleName,
286
241
  anon_role: structure.anonRole,
287
242
  is_public: structure.isPublic ?? false,
288
- schemas: structure.schema,
243
+ schemas: structure.schema
289
244
  });
290
245
  const settings = await resolveModuleSettings(defaultRegistry, loaderCtx);
291
246
  return {
@@ -295,63 +250,7 @@ const resolveScopedRoute = async (ctx) => {
295
250
  corsOrigins: settings.corsOrigins,
296
251
  databaseSettings: settings.databaseSettings,
297
252
  pubkeyChallengeSettings: settings.pubkeyChallengeSettings,
298
- webauthnSettings: settings.webauthnSettings,
299
- };
300
- };
301
- const resolveDomainLookup = async (ctx) => {
302
- const { opts, pool, domain, subdomain } = ctx;
303
- const isPublic = opts.api?.isPublic ?? false;
304
- log.debug(`[domain-lookup] domain=${domain} subdomain=${subdomain} isPublic=${isPublic}`);
305
- const row = await queryByDomain(pool, domain, subdomain, isPublic);
306
- if (!row) {
307
- log.debug(`[domain-lookup] No API found for domain=${domain} subdomain=${subdomain}`);
308
- return null;
309
- }
310
- const loaderCtx = buildLoaderContext(pool, opts, row);
311
- const settings = await resolveModuleSettings(defaultRegistry, loaderCtx);
312
- log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${settings.rlsModule ? 'found' : 'none'}, authSettings: ${settings.authSettings ? 'found' : 'none'}`);
313
- return toApiStructure(row, opts, settings);
314
- };
315
- const buildDevFallbackError = async (ctx, req) => {
316
- if (getNodeEnv() !== 'development')
317
- return null;
318
- const isPublic = ctx.opts.api?.isPublic ?? false;
319
- const apis = await queryApiList(ctx.pool, isPublic);
320
- if (!apis.length)
321
- return null;
322
- const host = req.get('host') || '';
323
- const portMatch = host.match(/:(\d+)$/);
324
- const port = portMatch ? portMatch[1] : '';
325
- const apiCards = apis.map((api) => {
326
- const domains = api.domains.length
327
- ? api.domains.map((d) => {
328
- const hostname = d.subdomain ? `${d.subdomain}.${d.domain}` : d.domain;
329
- const url = port ? `http://${hostname}:${port}/graphiql` : `http://${hostname}/graphiql`;
330
- return `<a href="${url}" style="color:#01A1FF;text-decoration:none;font-weight:500" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">${hostname}</a>`;
331
- }).join('<span style="color:#D4DCEA;margin:0 4px">·</span>')
332
- : '<span style="color:#8E9398;font-style:italic;font-size:11px">no domains</span>';
333
- const badge = api.is_public
334
- ? '<span style="color:#01A1FF;font-size:10px;font-weight:500">public</span>'
335
- : '<span style="color:#8E9398;font-size:10px">private</span>';
336
- return `
337
- <div style="background:#fff;border-radius:8px;padding:10px 14px;margin-bottom:6px;box-shadow:0 1px 3px rgba(0,0,0,0.04);border:1px solid #E8ECF0;display:flex;align-items:center;gap:12px;transition:background 0.15s" onmouseover="this.style.background='#FAFBFC'" onmouseout="this.style.background='#fff'">
338
- <div style="flex:1;min-width:0;display:flex;align-items:center;gap:8px;font-size:13px">
339
- <span style="font-weight:600;color:#232323;white-space:nowrap">${api.name}</span>
340
- <span style="color:#D4DCEA">→</span>
341
- ${domains}
342
- </div>
343
- <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
344
- <span style="color:#8E9398;font-size:11px;font-family:'SF Mono',Monaco,monospace">${api.dbname}</span>
345
- ${badge}
346
- </div>
347
- </div>`;
348
- }).join('');
349
- return {
350
- errorHtml: `
351
- <div style="text-align:left;max-width:600px;margin:0 auto">
352
- <p style="color:#8E9398;font-size:11px;margin-bottom:10px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px">Available APIs</p>
353
- ${apiCards}
354
- </div>`,
253
+ webauthnSettings: settings.webauthnSettings
355
254
  };
356
255
  };
357
256
  // =============================================================================
@@ -376,7 +275,7 @@ export const getApiConfig = async (opts, req) => {
376
275
  subdomain,
377
276
  cacheKey,
378
277
  headers: getRoutingHeaders(req),
379
- host: req.get('host') || '',
278
+ host: req.get('host') || ''
380
279
  };
381
280
  // Validate schemas upfront for modes that need them
382
281
  const apiOpts = opts.api || {};
@@ -396,8 +295,8 @@ export const getApiConfig = async (opts, req) => {
396
295
  const mode = determineMode(ctx);
397
296
  let result;
398
297
  switch (mode) {
399
- case 'services-disabled':
400
- result = resolveServicesDisabled(ctx);
298
+ case 'static':
299
+ result = resolveStatic(ctx);
401
300
  break;
402
301
  case 'schemata-header':
403
302
  result = await resolveSchemataHeader(ctx, validatedSchemas);
@@ -408,16 +307,8 @@ export const getApiConfig = async (opts, req) => {
408
307
  case 'meta-schema-header':
409
308
  result = resolveMetaSchemaHeader(ctx, validatedSchemas);
410
309
  break;
411
- case 'domain-lookup':
310
+ case 'scoped-route':
412
311
  result = await resolveScopedRoute(ctx);
413
- if (!result) {
414
- result = await resolveDomainLookup(ctx);
415
- }
416
- if (!result && apiOpts.isPublic) {
417
- const fallback = await buildDevFallbackError(ctx, req);
418
- if (fallback)
419
- return fallback;
420
- }
421
312
  break;
422
313
  }
423
314
  // Cache successful results
@@ -432,19 +323,20 @@ export const getApiConfig = async (opts, req) => {
432
323
  export const createApiMiddleware = (opts) => {
433
324
  return async (req, res, next) => {
434
325
  log.debug(`[api-middleware] ${req.method} ${req.path}`);
435
- // Fast path: services disabled
436
- if (opts.api?.enableServicesApi === false) {
437
- req.api = resolveServicesDisabled({
326
+ // Fast path: static single-tenant mode (scoped routing disabled) — no
327
+ // route resolution, expose the configured schemas directly.
328
+ if (!opts.api?.enableScopedRouting) {
329
+ req.api = resolveStatic({
438
330
  opts,
439
331
  pool: null,
440
332
  domain: '',
441
333
  subdomain: null,
442
- cacheKey: 'meta-api-off',
334
+ cacheKey: 'static',
443
335
  headers: {},
444
- host: '',
336
+ host: ''
445
337
  });
446
338
  req.databaseId = req.api.databaseId;
447
- req.svc_key = 'meta-api-off';
339
+ req.svc_key = 'static';
448
340
  return next();
449
341
  }
450
342
  try {
@@ -1,8 +1,8 @@
1
+ import './types'; // for Request type
1
2
  import { Logger } from '@pgpmjs/logger';
2
3
  import { svcCache } from '@pgpmjs/server-utils';
3
4
  import { graphileCache } from 'graphile-cache';
4
5
  import { getPgPool } from 'pg-cache';
5
- import './types'; // for Request type
6
6
  const log = new Logger('flush');
7
7
  export const flush = async (req, res, next) => {
8
8
  if (req.url === '/flush') {
@@ -28,19 +28,13 @@ export const flushService = async (opts, databaseId) => {
28
28
  }
29
29
  });
30
30
  }
31
- const svc = await pgPool.query(`SELECT *
32
- FROM services_public.domains
31
+ const svc = await pgPool.query(`SELECT hostname
32
+ FROM constructive_routing_public.domains
33
33
  WHERE database_id = $1`, [databaseId]);
34
34
  if (svc.rowCount === 0)
35
35
  return;
36
36
  for (const row of svc.rows) {
37
- let key;
38
- if (row.domain && !row.subdomain) {
39
- key = row.domain;
40
- }
41
- else if (row.domain && row.subdomain) {
42
- key = `${row.subdomain}.${row.domain}`;
43
- }
37
+ const key = row.hostname || undefined;
44
38
  if (key) {
45
39
  graphileCache.delete(key);
46
40
  svcCache.delete(key);
@@ -1,3 +1,4 @@
1
+ import './types'; // for Request type
1
2
  import crypto from 'node:crypto';
2
3
  import { getNodeEnv } from '@pgpmjs/env';
3
4
  import { Logger } from '@pgpmjs/logger';
@@ -6,11 +7,10 @@ import { createFunctionBindingsPlugin } from 'graphile-function-bindings';
6
7
  import { createConstructivePreset, makePgService } from 'graphile-settings';
7
8
  import { getPgPool } from 'pg-cache';
8
9
  import { getPgEnvOptions } from 'pg-env';
9
- import './types'; // for Request type
10
10
  import { isGraphqlObservabilityEnabled } from '../diagnostics/observability';
11
11
  import { HandlerCreationError } from '../errors/api-errors';
12
- import { observeGraphileBuild } from './observability/graphile-build-stats';
13
12
  import { AuthCookiePlugin } from '../plugins/auth-cookie-plugin';
13
+ import { observeGraphileBuild } from './observability/graphile-build-stats';
14
14
  const maskErrorLog = new Logger('graphile:maskError');
15
15
  const SAFE_ERROR_CODES = new Set([
16
16
  // GraphQL standard
@@ -115,7 +115,7 @@ const SAFE_ERROR_CODES = new Set([
115
115
  '23503', // foreign_key_violation
116
116
  '23502', // not_null_violation
117
117
  '23514', // check_violation
118
- '23P01', // exclusion_violation
118
+ '23P01' // exclusion_violation
119
119
  ]);
120
120
  /**
121
121
  * Production-aware error masking function.
@@ -142,8 +142,8 @@ const maskError = (error) => {
142
142
  message: `An unexpected error occurred. Reference: ${errorId}`,
143
143
  extensions: {
144
144
  code: 'INTERNAL_SERVER_ERROR',
145
- errorId,
146
- },
145
+ errorId
146
+ }
147
147
  };
148
148
  };
149
149
  // =============================================================================
@@ -204,24 +204,24 @@ const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings, apiId,
204
204
  definitionsTable: m.definitionsTableName,
205
205
  invocationsSchema: m.invocationsSchemaName,
206
206
  invocationsTable: m.invocationsTableName,
207
- invocationsEntityField: m.invocationsEntityField,
208
- })),
209
- }),
207
+ invocationsEntityField: m.invocationsEntityField
208
+ }))
209
+ })
210
210
  ]
211
- : []),
211
+ : [])
212
212
  ],
213
213
  pgServices: [
214
214
  makePgService({
215
215
  pool,
216
- schemas,
217
- }),
216
+ schemas
217
+ })
218
218
  ],
219
219
  grafserv: {
220
220
  graphqlPath: '/graphql',
221
221
  graphiqlPath: '/graphiql',
222
222
  graphiql: true,
223
223
  graphiqlOnGraphQLGET: false,
224
- maskError,
224
+ maskError
225
225
  },
226
226
  grafast: {
227
227
  explain: process.env.NODE_ENV === 'development',
@@ -234,8 +234,9 @@ const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings, apiId,
234
234
  context['jwt.claims.database_id'] = req.databaseId;
235
235
  }
236
236
  // API provenance — which API surface this request arrived through.
237
- // Derived server-side from hostname -> services_public.domains -> api_id;
238
- // never taken from client-supplied headers, body, or token payload.
237
+ // Derived server-side by resolving the hostname through the scoped
238
+ // routing plane (resolve_route -> api_id); never taken from
239
+ // client-supplied headers, body, or token payload.
239
240
  if (req.api?.apiId) {
240
241
  context['jwt.claims.api_id'] = req.api.apiId;
241
242
  }
@@ -256,7 +257,7 @@ const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings, apiId,
256
257
  role: roleName,
257
258
  'jwt.claims.token_id': req.token.id,
258
259
  'jwt.claims.user_id': req.token.user_id,
259
- ...context,
260
+ ...context
260
261
  };
261
262
  if (req.token.session_id) {
262
263
  pgSettings['jwt.claims.session_id'] = req.token.session_id;
@@ -283,16 +284,16 @@ const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings, apiId,
283
284
  }
284
285
  const anonSettings = {
285
286
  role: anonRole,
286
- ...context,
287
+ ...context
287
288
  };
288
289
  if (req?.requestId) {
289
290
  anonSettings['request.id'] = req.requestId;
290
291
  }
291
292
  return {
292
- pgSettings: anonSettings,
293
+ pgSettings: anonSettings
293
294
  };
294
- },
295
- },
295
+ }
296
+ }
296
297
  };
297
298
  };
298
299
  export const graphile = (opts) => {
@@ -355,7 +356,7 @@ export const graphile = (opts) => {
355
356
  log.info(`${label} Building PostGraphile v5 handler key=${key} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
356
357
  const pgConfig = getPgEnvOptions({
357
358
  ...opts.pg,
358
- database: dbname,
359
+ database: dbname
359
360
  });
360
361
  // Route through pg-cache so the pool is tracked and can be cleaned up
361
362
  // properly, preventing leaked connections during database teardown.
@@ -366,11 +367,11 @@ export const graphile = (opts) => {
366
367
  const creationPromise = observeGraphileBuild({
367
368
  cacheKey: key,
368
369
  serviceKey: key,
369
- databaseId: api.databaseId ?? null,
370
+ databaseId: api.databaseId ?? null
370
371
  }, () => createGraphileInstance({
371
372
  preset,
372
373
  cacheKey: key,
373
- enableRealtime: api.databaseSettings?.enableRealtime,
374
+ enableRealtime: api.databaseSettings?.enableRealtime
374
375
  }), { enabled: observabilityEnabled });
375
376
  creating.set(key, creationPromise);
376
377
  try {
@@ -383,7 +384,7 @@ export const graphile = (opts) => {
383
384
  log.error(`${label} Failed to create PostGraphile[${key}]:`, error);
384
385
  throw new HandlerCreationError(`Failed to create handler for ${key}: ${error instanceof Error ? error.message : String(error)}`, {
385
386
  cacheKey: key,
386
- cause: error instanceof Error ? error.message : String(error),
387
+ cause: error instanceof Error ? error.message : String(error)
387
388
  });
388
389
  }
389
390
  finally {
@@ -6,8 +6,8 @@ const isValidSchemaName = (name) => /^[a-z_][a-z0-9_]*$/.test(name);
6
6
  * Resolve a hostname through the compiled scoped-routing plane (host-only:
7
7
  * path/method routing belongs to Traefik/Ingress, not the server).
8
8
  * Returns null when there is no match (route_binding_id IS NULL) or when the
9
- * resolver is not installed in the target database — callers fall back to the
10
- * legacy services_public lookup in both cases.
9
+ * resolver is not installed in the target database — in both cases the caller
10
+ * treats it as a hard no-match (→ 404); there is no legacy fallback.
11
11
  */
12
12
  export const resolveRoute = async (pool, schema, host) => {
13
13
  if (!isValidSchemaName(schema)) {
@@ -26,19 +26,18 @@ export const resolveRoute = async (pool, schema, host) => {
26
26
  catch (error) {
27
27
  const err = error;
28
28
  // 42883 undefined_function / 3F000 invalid_schema_name: resolver not
29
- // installed in this database — treat as no-match so the caller falls back.
29
+ // installed in this database — treat as a hard no-match.
30
30
  if (err.code === '42883' || err.code === '3F000') {
31
- log.debug(`[resolve-route] resolver not installed (${err.code}); falling back`);
31
+ log.debug(`[resolve-route] resolver not installed (${err.code}); no match`);
32
32
  return null;
33
33
  }
34
34
  throw error;
35
35
  }
36
36
  };
37
37
  /**
38
- * Map a resolved api-target route onto the legacy ApiStructure shape consumed
39
- * by the rest of the middleware chain. Returns null when the route target is
40
- * not an api surface or its resolved_config lacks the api essentials the
41
- * caller falls back to the legacy lookup.
38
+ * Map a resolved api-target route onto the ApiStructure shape consumed by the
39
+ * rest of the middleware chain. Returns null when the route target is not an
40
+ * api surface or its resolved_config lacks the api essentials (→ 404).
42
41
  */
43
42
  export const routeToApiStructure = (route, opts) => {
44
43
  if (route.target_module !== 'apis' && route.target_module !== 'api') {
@@ -46,7 +45,7 @@ export const routeToApiStructure = (route, opts) => {
46
45
  }
47
46
  const config = (route.resolved_config ?? {});
48
47
  if (!config.dbname || !config.schemas?.length) {
49
- log.debug('[resolve-route] api target missing dbname/schemas in resolved_config; falling back');
48
+ log.debug('[resolve-route] api target missing dbname/schemas in resolved_config; no match');
50
49
  return null;
51
50
  }
52
51
  return {
@@ -58,6 +57,6 @@ export const routeToApiStructure = (route, opts) => {
58
57
  apiModules: [],
59
58
  domains: [],
60
59
  databaseId: config.database_id,
61
- isPublic: config.is_public ?? (opts.api?.isPublic ?? false),
60
+ isPublic: config.is_public ?? (opts.api?.isPublic ?? false)
62
61
  };
63
62
  };