@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.
package/esm/server.js CHANGED
@@ -1,35 +1,35 @@
1
1
  import { createCsrfMiddleware } from '@constructive-io/csrf';
2
+ import { createContextMiddleware, createDefaultRegistry, requestIdMiddleware } from '@constructive-io/express-context';
2
3
  import { getEnvOptions } from '@constructive-io/graphql-env';
4
+ import { middleware as parseDomains } from '@constructive-io/url-domains';
3
5
  import { Logger } from '@pgpmjs/logger';
4
6
  import { healthz, poweredBy, svcCache, trustProxy } from '@pgpmjs/server-utils';
5
- import { middleware as parseDomains } from '@constructive-io/url-domains';
7
+ import { createAgenticRouter } from 'agentic-server';
6
8
  import cookieParser from 'cookie-parser';
7
9
  import express from 'express';
10
+ import { closeAllCaches, graphileCache } from 'graphile-cache';
8
11
  import graphqlUpload from 'graphql-upload';
9
- import { graphileCache, closeAllCaches } from 'graphile-cache';
10
12
  import { getPgPool } from 'pg-cache';
11
13
  import requestIp from 'request-ip';
12
14
  import { closeDebugDatabasePools } from './diagnostics/debug-db-snapshot';
13
- import { isDevelopmentObservabilityMode, isGraphqlObservabilityEnabled, isGraphqlObservabilityRequested, isLoopbackHost, } from './diagnostics/observability';
15
+ import { startDebugSampler } from './diagnostics/debug-sampler';
16
+ import { isDevelopmentObservabilityMode, isGraphqlObservabilityEnabled, isGraphqlObservabilityRequested, isLoopbackHost } from './diagnostics/observability';
14
17
  import { createApiMiddleware } from './middleware/api';
15
18
  import { createAuthenticateMiddleware } from './middleware/auth';
19
+ // Auth cookie handling is done via AuthCookiePlugin in grafserv
20
+ import { createCaptchaMiddleware } from './middleware/captcha';
21
+ import { parseCookieValue, SESSION_COOKIE_NAME } from './middleware/cookie';
16
22
  import { cors } from './middleware/cors';
17
23
  import { errorHandler, notFoundHandler } from './middleware/error-handler';
18
24
  import { favicon } from './middleware/favicon';
19
- import { createFnRouter } from './middleware/fn';
20
25
  import { flush, flushService } from './middleware/flush';
26
+ import { createFnRouter } from './middleware/fn';
21
27
  import { graphile } from './middleware/graphile';
22
28
  import { multipartBridge } from './middleware/multipart-bridge';
23
29
  import { createDebugDatabaseMiddleware } from './middleware/observability/debug-db';
24
30
  import { debugMemory } from './middleware/observability/debug-memory';
25
31
  import { localObservabilityOnly } from './middleware/observability/guard';
26
32
  import { createRequestLogger } from './middleware/observability/request-logger';
27
- // Auth cookie handling is done via AuthCookiePlugin in grafserv
28
- import { createCaptchaMiddleware } from './middleware/captcha';
29
- import { parseCookieValue, SESSION_COOKIE_NAME } from './middleware/cookie';
30
- import { createAgenticRouter } from 'agentic-server';
31
- import { createContextMiddleware, createDefaultRegistry, requestIdMiddleware } from '@constructive-io/express-context';
32
- import { startDebugSampler } from './diagnostics/debug-sampler';
33
33
  const log = new Logger('server');
34
34
  /**
35
35
  * Creates and starts a GraphQL server instance
@@ -85,12 +85,13 @@ class Server {
85
85
  serverHost: effectiveOpts.server?.host,
86
86
  serverPort: effectiveOpts.server?.port,
87
87
  apiIsPublic: apiOpts.isPublic,
88
- enableServicesApi: apiOpts.enableServicesApi,
88
+ enableScopedRouting: apiOpts.enableScopedRouting,
89
+ scopedRoutingSchema: apiOpts.scopedRoutingSchema,
89
90
  metaSchemas: apiOpts.metaSchemas?.join(',') || 'default',
90
91
  exposedSchemas: apiOpts.exposedSchemas?.join(',') || 'none',
91
92
  anonRole: apiOpts.anonRole,
92
93
  roleName: apiOpts.roleName,
93
- observabilityEnabled,
94
+ observabilityEnabled
94
95
  });
95
96
  if (observabilityRequested && !observabilityEnabled) {
96
97
  const reasons = [];
@@ -129,7 +130,7 @@ class Server {
129
130
  app.use(cors(fallbackOrigin));
130
131
  app.use('/graphql', graphqlUpload.graphqlUploadExpress({
131
132
  maxFileSize: 10 * 1024 * 1024, // 10 MB
132
- maxFiles: 10,
133
+ maxFiles: 10
133
134
  }));
134
135
  // Rewrite Content-Type after graphql-upload so grafserv accepts the request
135
136
  app.use('/graphql', multipartBridge);
@@ -147,8 +148,8 @@ class Server {
147
148
  cookieOptions: {
148
149
  httpOnly: false, // SPA clients need to read this via document.cookie
149
150
  secure: process.env.NODE_ENV === 'production',
150
- sameSite: 'lax',
151
- },
151
+ sameSite: 'lax'
152
+ }
152
153
  });
153
154
  const csrfProtect = (req, res, next) => {
154
155
  // Skip CSRF for Bearer token auth
@@ -1,6 +1,6 @@
1
+ import './types';
1
2
  import { NextFunction, Request, Response } from 'express';
2
3
  import { ApiConfigResult, ApiOptions } from '../types';
3
- import './types';
4
4
  export declare const getSubdomain: (subdomains: string[]) => string | null;
5
5
  export declare const getSvcKey: (opts: ApiOptions, req: Request) => string;
6
6
  export declare const getApiConfig: (opts: ApiOptions, req: Request) => Promise<ApiConfigResult>;
package/middleware/api.js CHANGED
@@ -4,16 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createApiMiddleware = exports.getApiConfig = exports.getSvcKey = exports.getSubdomain = void 0;
7
- const env_1 = require("@pgpmjs/env");
7
+ require("./types");
8
+ const express_context_1 = require("@constructive-io/express-context");
9
+ const url_domains_1 = require("@constructive-io/url-domains");
8
10
  const logger_1 = require("@pgpmjs/logger");
9
11
  const server_utils_1 = require("@pgpmjs/server-utils");
10
- const url_domains_1 = require("@constructive-io/url-domains");
11
- const express_context_1 = require("@constructive-io/express-context");
12
12
  const pg_cache_1 = require("pg-cache");
13
13
  const _50x_1 = __importDefault(require("../errors/50x"));
14
14
  const _404_message_1 = __importDefault(require("../errors/404-message"));
15
15
  const routing_1 = require("./routing");
16
- require("./types");
17
16
  const log = new logger_1.Logger('api');
18
17
  // =============================================================================
19
18
  // Module Loader Registry (replaces inline SQL queries for per-db config)
@@ -22,87 +21,49 @@ const defaultRegistry = (0, express_context_1.createDefaultRegistry)();
22
21
  // =============================================================================
23
22
  // SQL Queries (API resolution only — module queries now live in loaders)
24
23
  // =============================================================================
25
- const DOMAIN_LOOKUP_SQL = `
24
+ // Private-header X-Api-Name lookup against the scoped routing plane.
25
+ // `is_published` is the routing-plane analog of the legacy `is_public` column.
26
+ const SCOPED_API_NAME_LOOKUP_SQL = `
26
27
  SELECT
27
28
  a.id as api_id,
28
29
  a.database_id,
29
30
  a.dbname,
30
31
  a.role_name,
31
32
  a.anon_role,
32
- a.is_public,
33
+ a.is_published as is_public,
33
34
  COALESCE(array_agg(s.schema_name) FILTER (WHERE s.schema_name IS NOT NULL), '{}') as schemas
34
- FROM services_public.domains d
35
- JOIN services_public.apis a ON d.api_id = a.id
36
- LEFT JOIN services_public.api_schemas aps ON a.id = aps.api_id
37
- LEFT JOIN metaschema_public.schema s ON aps.schema_id = s.id
38
- WHERE d.domain = $1
39
- AND (($2::text IS NULL AND d.subdomain IS NULL) OR d.subdomain = $2)
40
- AND a.is_public = $3
41
- GROUP BY a.id, a.database_id, a.dbname, a.role_name, a.anon_role, a.is_public
42
- LIMIT 1
43
- `;
44
- const API_NAME_LOOKUP_SQL = `
45
- SELECT
46
- a.id as api_id,
47
- a.database_id,
48
- a.dbname,
49
- a.role_name,
50
- a.anon_role,
51
- a.is_public,
52
- COALESCE(array_agg(s.schema_name) FILTER (WHERE s.schema_name IS NOT NULL), '{}') as schemas
53
- FROM services_public.apis a
54
- LEFT JOIN services_public.api_schemas aps ON a.id = aps.api_id
35
+ FROM constructive_routing_public.apis a
36
+ LEFT JOIN constructive_routing_public.api_schemas aps ON a.id = aps.api_id
55
37
  LEFT JOIN metaschema_public.schema s ON aps.schema_id = s.id
56
38
  WHERE a.database_id = $1
57
39
  AND a.name = $2
58
- AND a.is_public = $3
59
- GROUP BY a.id, a.database_id, a.dbname, a.role_name, a.anon_role, a.is_public
40
+ AND a.is_published = $3
41
+ GROUP BY a.id, a.database_id, a.dbname, a.role_name, a.anon_role, a.is_published
60
42
  LIMIT 1
61
43
  `;
62
- const API_LIST_SQL = `
63
- SELECT
64
- a.id,
65
- a.database_id,
66
- a.name,
67
- a.dbname,
68
- a.role_name,
69
- a.anon_role,
70
- a.is_public,
71
- COALESCE(
72
- json_agg(
73
- json_build_object('domain', d.domain, 'subdomain', d.subdomain)
74
- ) FILTER (WHERE d.domain IS NOT NULL),
75
- '[]'
76
- ) as domains
77
- FROM services_public.apis a
78
- LEFT JOIN services_public.domains d ON a.id = d.api_id
79
- WHERE a.is_public = $1
80
- GROUP BY a.id, a.database_id, a.name, a.dbname, a.role_name, a.anon_role, a.is_public
81
- LIMIT 100
82
- `;
83
44
  /**
84
45
  * Build a LoaderContext from the API row and options.
85
46
  * This is used to resolve per-database module settings via the loader registry.
86
47
  */
87
- const buildLoaderContext = (servicesPool, opts, row) => ({
88
- servicesPool,
48
+ const buildLoaderContext = (routingPool, opts, row) => ({
49
+ routingPool,
89
50
  tenantPool: (0, pg_cache_1.getPgPool)({ ...opts.pg, database: row.dbname }),
90
51
  databaseId: row.database_id,
91
52
  apiId: row.api_id,
92
- dbname: row.dbname,
53
+ dbname: row.dbname
93
54
  });
94
55
  /**
95
56
  * Resolve all per-database module settings in parallel via the loader registry.
96
57
  * Each loader independently caches by databaseId — repeated calls are cheap.
97
58
  */
98
59
  const resolveModuleSettings = async (registry, ctx) => {
99
- const [rlsModule, authSettings, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings,] = await Promise.all([
60
+ const [rlsModule, authSettings, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings] = await Promise.all([
100
61
  registry.resolve('rlsModule', ctx),
101
62
  registry.resolve('authSettings', ctx),
102
63
  registry.resolve('corsOrigins', ctx),
103
64
  registry.resolve('databaseSettings', ctx),
104
65
  registry.resolve('pubkeyChallengeSettings', ctx),
105
- registry.resolve('webauthnSettings', ctx),
66
+ registry.resolve('webauthnSettings', ctx)
106
67
  ]);
107
68
  return {
108
69
  rlsModule,
@@ -110,7 +71,7 @@ const resolveModuleSettings = async (registry, ctx) => {
110
71
  corsOrigins,
111
72
  databaseSettings,
112
73
  pubkeyChallengeSettings,
113
- webauthnSettings,
74
+ webauthnSettings
114
75
  };
115
76
  };
116
77
  // =============================================================================
@@ -131,14 +92,14 @@ const getRoutingHeaders = (req) => ({
131
92
  schemata: req.get('X-Schemata'),
132
93
  apiName: req.get('X-Api-Name'),
133
94
  metaSchema: req.get('X-Meta-Schema'),
134
- databaseId: req.get('X-Database-Id'),
95
+ databaseId: req.get('X-Database-Id')
135
96
  });
136
97
  const getUrlDomains = (req) => {
137
98
  const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
138
99
  const parsed = (0, url_domains_1.parseUrl)(fullUrl);
139
100
  return {
140
101
  domain: parsed.domain ?? '',
141
- subdomains: parsed.subdomains ?? [],
102
+ subdomains: parsed.subdomains ?? []
142
103
  };
143
104
  };
144
105
  const getSubdomain = (subdomains) => {
@@ -180,7 +141,7 @@ const toApiStructure = (row, opts, settings = {}) => ({
180
141
  corsOrigins: settings.corsOrigins,
181
142
  databaseSettings: settings.databaseSettings,
182
143
  pubkeyChallengeSettings: settings.pubkeyChallengeSettings,
183
- webauthnSettings: settings.webauthnSettings,
144
+ webauthnSettings: settings.webauthnSettings
184
145
  });
185
146
  const createAdminStructure = (opts, schemas, databaseId) => ({
186
147
  dbname: opts.pg?.database ?? '',
@@ -190,7 +151,7 @@ const createAdminStructure = (opts, schemas, databaseId) => ({
190
151
  apiModules: [],
191
152
  domains: [],
192
153
  databaseId,
193
- isPublic: false,
154
+ isPublic: false
194
155
  });
195
156
  // =============================================================================
196
157
  // Database Queries (API resolution only)
@@ -199,31 +160,25 @@ const validateSchemata = async (pool, schemas) => {
199
160
  const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemas]);
200
161
  return result.rows.map((row) => row.schema_name);
201
162
  };
202
- const queryByDomain = async (pool, domain, subdomain, isPublic) => {
203
- const result = await pool.query(DOMAIN_LOOKUP_SQL, [domain, subdomain, isPublic]);
204
- return result.rows[0] ?? null;
205
- };
206
163
  const queryByApiName = async (pool, databaseId, name, isPublic) => {
207
- const result = await pool.query(API_NAME_LOOKUP_SQL, [databaseId, name, isPublic]);
164
+ const result = await pool.query(SCOPED_API_NAME_LOOKUP_SQL, [databaseId, name, isPublic]);
208
165
  return result.rows[0] ?? null;
209
166
  };
210
- const queryApiList = async (pool, isPublic) => {
211
- const result = await pool.query(API_LIST_SQL, [isPublic]);
212
- return result.rows;
213
- };
214
167
  // =============================================================================
215
168
  // Resolution Logic
216
169
  // =============================================================================
217
170
  const determineMode = (ctx) => {
218
171
  const { opts, headers } = ctx;
219
- if (opts.api?.enableServicesApi === false)
220
- return 'services-disabled';
172
+ // Static single-tenant mode: scoped routing off — expose configured schemas
173
+ // directly with no route resolution.
174
+ if (!opts.api?.enableScopedRouting)
175
+ return 'static';
221
176
  if (opts.api?.isPublic === false) {
222
- return getPrivateHeaderMode(headers) ?? 'domain-lookup';
177
+ return getPrivateHeaderMode(headers) ?? 'scoped-route';
223
178
  }
224
- return 'domain-lookup';
179
+ return 'scoped-route';
225
180
  };
226
- const resolveServicesDisabled = (ctx) => {
181
+ const resolveStatic = (ctx) => {
227
182
  const { opts } = ctx;
228
183
  return {
229
184
  dbname: opts.pg?.database ?? '',
@@ -233,7 +188,7 @@ const resolveServicesDisabled = (ctx) => {
233
188
  apiModules: [],
234
189
  domains: [],
235
190
  databaseId: opts.api?.defaultDatabaseId,
236
- isPublic: false,
191
+ isPublic: false
237
192
  };
238
193
  };
239
194
  const resolveSchemataHeader = async (ctx, validatedSchemas) => {
@@ -265,12 +220,12 @@ const resolveMetaSchemaHeader = (ctx, validatedSchemas) => {
265
220
  return createAdminStructure(ctx.opts, validatedSchemas, ctx.headers.databaseId);
266
221
  };
267
222
  /**
268
- * Scoped routing plane resolution (additive, host-only): one indexed
269
- * resolve_route() call against the compiled hostname/route bindings.
270
- * Path/method routing belongs to Traefik/Ingress — the server only maps
271
- * host → tenant/api/db/role. Returns null (fall back to the legacy
272
- * services_public lookup) when disabled, unmatched, resolver not installed,
273
- * or the target is not an api surface.
223
+ * Scoped routing plane resolution (host-only): one indexed resolve_route()
224
+ * call against the compiled hostname/route bindings. Path/method routing
225
+ * belongs to Traefik/Ingress — the server only maps host → tenant/api/db/role.
226
+ * This is the sole host resolver. Returns null ( 404) when disabled,
227
+ * unmatched, the resolver is not installed, or the target is not an api
228
+ * surface. There is no legacy fallback.
274
229
  */
275
230
  const resolveScopedRoute = async (ctx) => {
276
231
  const { opts, pool, host } = ctx;
@@ -293,7 +248,7 @@ const resolveScopedRoute = async (ctx) => {
293
248
  role_name: structure.roleName,
294
249
  anon_role: structure.anonRole,
295
250
  is_public: structure.isPublic ?? false,
296
- schemas: structure.schema,
251
+ schemas: structure.schema
297
252
  });
298
253
  const settings = await resolveModuleSettings(defaultRegistry, loaderCtx);
299
254
  return {
@@ -303,63 +258,7 @@ const resolveScopedRoute = async (ctx) => {
303
258
  corsOrigins: settings.corsOrigins,
304
259
  databaseSettings: settings.databaseSettings,
305
260
  pubkeyChallengeSettings: settings.pubkeyChallengeSettings,
306
- webauthnSettings: settings.webauthnSettings,
307
- };
308
- };
309
- const resolveDomainLookup = async (ctx) => {
310
- const { opts, pool, domain, subdomain } = ctx;
311
- const isPublic = opts.api?.isPublic ?? false;
312
- log.debug(`[domain-lookup] domain=${domain} subdomain=${subdomain} isPublic=${isPublic}`);
313
- const row = await queryByDomain(pool, domain, subdomain, isPublic);
314
- if (!row) {
315
- log.debug(`[domain-lookup] No API found for domain=${domain} subdomain=${subdomain}`);
316
- return null;
317
- }
318
- const loaderCtx = buildLoaderContext(pool, opts, row);
319
- const settings = await resolveModuleSettings(defaultRegistry, loaderCtx);
320
- log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${settings.rlsModule ? 'found' : 'none'}, authSettings: ${settings.authSettings ? 'found' : 'none'}`);
321
- return toApiStructure(row, opts, settings);
322
- };
323
- const buildDevFallbackError = async (ctx, req) => {
324
- if ((0, env_1.getNodeEnv)() !== 'development')
325
- return null;
326
- const isPublic = ctx.opts.api?.isPublic ?? false;
327
- const apis = await queryApiList(ctx.pool, isPublic);
328
- if (!apis.length)
329
- return null;
330
- const host = req.get('host') || '';
331
- const portMatch = host.match(/:(\d+)$/);
332
- const port = portMatch ? portMatch[1] : '';
333
- const apiCards = apis.map((api) => {
334
- const domains = api.domains.length
335
- ? api.domains.map((d) => {
336
- const hostname = d.subdomain ? `${d.subdomain}.${d.domain}` : d.domain;
337
- const url = port ? `http://${hostname}:${port}/graphiql` : `http://${hostname}/graphiql`;
338
- 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>`;
339
- }).join('<span style="color:#D4DCEA;margin:0 4px">·</span>')
340
- : '<span style="color:#8E9398;font-style:italic;font-size:11px">no domains</span>';
341
- const badge = api.is_public
342
- ? '<span style="color:#01A1FF;font-size:10px;font-weight:500">public</span>'
343
- : '<span style="color:#8E9398;font-size:10px">private</span>';
344
- return `
345
- <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'">
346
- <div style="flex:1;min-width:0;display:flex;align-items:center;gap:8px;font-size:13px">
347
- <span style="font-weight:600;color:#232323;white-space:nowrap">${api.name}</span>
348
- <span style="color:#D4DCEA">→</span>
349
- ${domains}
350
- </div>
351
- <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
352
- <span style="color:#8E9398;font-size:11px;font-family:'SF Mono',Monaco,monospace">${api.dbname}</span>
353
- ${badge}
354
- </div>
355
- </div>`;
356
- }).join('');
357
- return {
358
- errorHtml: `
359
- <div style="text-align:left;max-width:600px;margin:0 auto">
360
- <p style="color:#8E9398;font-size:11px;margin-bottom:10px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px">Available APIs</p>
361
- ${apiCards}
362
- </div>`,
261
+ webauthnSettings: settings.webauthnSettings
363
262
  };
364
263
  };
365
264
  // =============================================================================
@@ -384,7 +283,7 @@ const getApiConfig = async (opts, req) => {
384
283
  subdomain,
385
284
  cacheKey,
386
285
  headers: getRoutingHeaders(req),
387
- host: req.get('host') || '',
286
+ host: req.get('host') || ''
388
287
  };
389
288
  // Validate schemas upfront for modes that need them
390
289
  const apiOpts = opts.api || {};
@@ -404,8 +303,8 @@ const getApiConfig = async (opts, req) => {
404
303
  const mode = determineMode(ctx);
405
304
  let result;
406
305
  switch (mode) {
407
- case 'services-disabled':
408
- result = resolveServicesDisabled(ctx);
306
+ case 'static':
307
+ result = resolveStatic(ctx);
409
308
  break;
410
309
  case 'schemata-header':
411
310
  result = await resolveSchemataHeader(ctx, validatedSchemas);
@@ -416,16 +315,8 @@ const getApiConfig = async (opts, req) => {
416
315
  case 'meta-schema-header':
417
316
  result = resolveMetaSchemaHeader(ctx, validatedSchemas);
418
317
  break;
419
- case 'domain-lookup':
318
+ case 'scoped-route':
420
319
  result = await resolveScopedRoute(ctx);
421
- if (!result) {
422
- result = await resolveDomainLookup(ctx);
423
- }
424
- if (!result && apiOpts.isPublic) {
425
- const fallback = await buildDevFallbackError(ctx, req);
426
- if (fallback)
427
- return fallback;
428
- }
429
320
  break;
430
321
  }
431
322
  // Cache successful results
@@ -441,19 +332,20 @@ exports.getApiConfig = getApiConfig;
441
332
  const createApiMiddleware = (opts) => {
442
333
  return async (req, res, next) => {
443
334
  log.debug(`[api-middleware] ${req.method} ${req.path}`);
444
- // Fast path: services disabled
445
- if (opts.api?.enableServicesApi === false) {
446
- req.api = resolveServicesDisabled({
335
+ // Fast path: static single-tenant mode (scoped routing disabled) — no
336
+ // route resolution, expose the configured schemas directly.
337
+ if (!opts.api?.enableScopedRouting) {
338
+ req.api = resolveStatic({
447
339
  opts,
448
340
  pool: null,
449
341
  domain: '',
450
342
  subdomain: null,
451
- cacheKey: 'meta-api-off',
343
+ cacheKey: 'static',
452
344
  headers: {},
453
- host: '',
345
+ host: ''
454
346
  });
455
347
  req.databaseId = req.api.databaseId;
456
- req.svc_key = 'meta-api-off';
348
+ req.svc_key = 'static';
457
349
  return next();
458
350
  }
459
351
  try {
@@ -1,5 +1,5 @@
1
+ import './types';
1
2
  import { ConstructiveOptions } from '@constructive-io/graphql-types';
2
3
  import { NextFunction, Request, Response } from 'express';
3
- import './types';
4
4
  export declare const flush: (req: Request, res: Response, next: NextFunction) => Promise<void>;
5
5
  export declare const flushService: (opts: ConstructiveOptions, databaseId: string) => Promise<void>;
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.flushService = exports.flush = void 0;
4
+ require("./types"); // for Request type
4
5
  const logger_1 = require("@pgpmjs/logger");
5
6
  const server_utils_1 = require("@pgpmjs/server-utils");
6
7
  const graphile_cache_1 = require("graphile-cache");
7
8
  const pg_cache_1 = require("pg-cache");
8
- require("./types"); // for Request type
9
9
  const log = new logger_1.Logger('flush');
10
10
  const flush = async (req, res, next) => {
11
11
  if (req.url === '/flush') {
@@ -32,19 +32,13 @@ const flushService = async (opts, databaseId) => {
32
32
  }
33
33
  });
34
34
  }
35
- const svc = await pgPool.query(`SELECT *
36
- FROM services_public.domains
35
+ const svc = await pgPool.query(`SELECT hostname
36
+ FROM constructive_routing_public.domains
37
37
  WHERE database_id = $1`, [databaseId]);
38
38
  if (svc.rowCount === 0)
39
39
  return;
40
40
  for (const row of svc.rows) {
41
- let key;
42
- if (row.domain && !row.subdomain) {
43
- key = row.domain;
44
- }
45
- else if (row.domain && row.subdomain) {
46
- key = `${row.subdomain}.${row.domain}`;
47
- }
41
+ const key = row.hostname || undefined;
48
42
  if (key) {
49
43
  graphile_cache_1.graphileCache.delete(key);
50
44
  server_utils_1.svcCache.delete(key);
@@ -1,6 +1,6 @@
1
+ import './types';
1
2
  import type { ConstructiveOptions } from '@constructive-io/graphql-types';
2
3
  import type { RequestHandler } from 'express';
3
- import './types';
4
4
  /**
5
5
  * Returns the number of currently in-flight handler creation operations.
6
6
  * Useful for monitoring and debugging.
@@ -7,6 +7,7 @@ exports.graphile = void 0;
7
7
  exports.getInFlightCount = getInFlightCount;
8
8
  exports.getInFlightKeys = getInFlightKeys;
9
9
  exports.clearInFlightMap = clearInFlightMap;
10
+ require("./types"); // for Request type
10
11
  const node_crypto_1 = __importDefault(require("node:crypto"));
11
12
  const env_1 = require("@pgpmjs/env");
12
13
  const logger_1 = require("@pgpmjs/logger");
@@ -15,11 +16,10 @@ const graphile_function_bindings_1 = require("graphile-function-bindings");
15
16
  const graphile_settings_1 = require("graphile-settings");
16
17
  const pg_cache_1 = require("pg-cache");
17
18
  const pg_env_1 = require("pg-env");
18
- require("./types"); // for Request type
19
19
  const observability_1 = require("../diagnostics/observability");
20
20
  const api_errors_1 = require("../errors/api-errors");
21
- const graphile_build_stats_1 = require("./observability/graphile-build-stats");
22
21
  const auth_cookie_plugin_1 = require("../plugins/auth-cookie-plugin");
22
+ const graphile_build_stats_1 = require("./observability/graphile-build-stats");
23
23
  const maskErrorLog = new logger_1.Logger('graphile:maskError');
24
24
  const SAFE_ERROR_CODES = new Set([
25
25
  // GraphQL standard
@@ -124,7 +124,7 @@ const SAFE_ERROR_CODES = new Set([
124
124
  '23503', // foreign_key_violation
125
125
  '23502', // not_null_violation
126
126
  '23514', // check_violation
127
- '23P01', // exclusion_violation
127
+ '23P01' // exclusion_violation
128
128
  ]);
129
129
  /**
130
130
  * Production-aware error masking function.
@@ -151,8 +151,8 @@ const maskError = (error) => {
151
151
  message: `An unexpected error occurred. Reference: ${errorId}`,
152
152
  extensions: {
153
153
  code: 'INTERNAL_SERVER_ERROR',
154
- errorId,
155
- },
154
+ errorId
155
+ }
156
156
  };
157
157
  };
158
158
  // =============================================================================
@@ -213,24 +213,24 @@ const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings, apiId,
213
213
  definitionsTable: m.definitionsTableName,
214
214
  invocationsSchema: m.invocationsSchemaName,
215
215
  invocationsTable: m.invocationsTableName,
216
- invocationsEntityField: m.invocationsEntityField,
217
- })),
218
- }),
216
+ invocationsEntityField: m.invocationsEntityField
217
+ }))
218
+ })
219
219
  ]
220
- : []),
220
+ : [])
221
221
  ],
222
222
  pgServices: [
223
223
  (0, graphile_settings_1.makePgService)({
224
224
  pool,
225
- schemas,
226
- }),
225
+ schemas
226
+ })
227
227
  ],
228
228
  grafserv: {
229
229
  graphqlPath: '/graphql',
230
230
  graphiqlPath: '/graphiql',
231
231
  graphiql: true,
232
232
  graphiqlOnGraphQLGET: false,
233
- maskError,
233
+ maskError
234
234
  },
235
235
  grafast: {
236
236
  explain: process.env.NODE_ENV === 'development',
@@ -243,8 +243,9 @@ const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings, apiId,
243
243
  context['jwt.claims.database_id'] = req.databaseId;
244
244
  }
245
245
  // API provenance — which API surface this request arrived through.
246
- // Derived server-side from hostname -> services_public.domains -> api_id;
247
- // never taken from client-supplied headers, body, or token payload.
246
+ // Derived server-side by resolving the hostname through the scoped
247
+ // routing plane (resolve_route -> api_id); never taken from
248
+ // client-supplied headers, body, or token payload.
248
249
  if (req.api?.apiId) {
249
250
  context['jwt.claims.api_id'] = req.api.apiId;
250
251
  }
@@ -265,7 +266,7 @@ const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings, apiId,
265
266
  role: roleName,
266
267
  'jwt.claims.token_id': req.token.id,
267
268
  'jwt.claims.user_id': req.token.user_id,
268
- ...context,
269
+ ...context
269
270
  };
270
271
  if (req.token.session_id) {
271
272
  pgSettings['jwt.claims.session_id'] = req.token.session_id;
@@ -292,16 +293,16 @@ const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings, apiId,
292
293
  }
293
294
  const anonSettings = {
294
295
  role: anonRole,
295
- ...context,
296
+ ...context
296
297
  };
297
298
  if (req?.requestId) {
298
299
  anonSettings['request.id'] = req.requestId;
299
300
  }
300
301
  return {
301
- pgSettings: anonSettings,
302
+ pgSettings: anonSettings
302
303
  };
303
- },
304
- },
304
+ }
305
+ }
305
306
  };
306
307
  };
307
308
  const graphile = (opts) => {
@@ -364,7 +365,7 @@ const graphile = (opts) => {
364
365
  log.info(`${label} Building PostGraphile v5 handler key=${key} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}`);
365
366
  const pgConfig = (0, pg_env_1.getPgEnvOptions)({
366
367
  ...opts.pg,
367
- database: dbname,
368
+ database: dbname
368
369
  });
369
370
  // Route through pg-cache so the pool is tracked and can be cleaned up
370
371
  // properly, preventing leaked connections during database teardown.
@@ -375,11 +376,11 @@ const graphile = (opts) => {
375
376
  const creationPromise = (0, graphile_build_stats_1.observeGraphileBuild)({
376
377
  cacheKey: key,
377
378
  serviceKey: key,
378
- databaseId: api.databaseId ?? null,
379
+ databaseId: api.databaseId ?? null
379
380
  }, () => (0, graphile_cache_1.createGraphileInstance)({
380
381
  preset,
381
382
  cacheKey: key,
382
- enableRealtime: api.databaseSettings?.enableRealtime,
383
+ enableRealtime: api.databaseSettings?.enableRealtime
383
384
  }), { enabled: observabilityEnabled });
384
385
  creating.set(key, creationPromise);
385
386
  try {
@@ -392,7 +393,7 @@ const graphile = (opts) => {
392
393
  log.error(`${label} Failed to create PostGraphile[${key}]:`, error);
393
394
  throw new api_errors_1.HandlerCreationError(`Failed to create handler for ${key}: ${error instanceof Error ? error.message : String(error)}`, {
394
395
  cacheKey: key,
395
- cause: error instanceof Error ? error.message : String(error),
396
+ cause: error instanceof Error ? error.message : String(error)
396
397
  });
397
398
  }
398
399
  finally {