@constructive-io/graphql-server 5.32.1 → 5.32.2

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,17 +1,10 @@
1
1
  import { resolvePreset } from 'graphile-config';
2
+ import { ScopedIntrospectionPreset } from 'graphile-scoped-introspection';
2
3
  import { makePgService } from 'graphile-settings';
3
- const SCOPED_INTROSPECTION_PLUGIN = 'ConstructivePgIntrospectionPlugin';
4
- let scopedIntrospectionPresetPromise;
5
- const loadScopedIntrospectionPreset = () => {
6
- scopedIntrospectionPresetPromise ??=
7
- import('graphile-scoped-introspection').then(({ ScopedIntrospectionPreset }) => ScopedIntrospectionPreset);
8
- return scopedIntrospectionPresetPromise;
9
- };
4
+ const SERVICE_NAME = 'main';
10
5
  /**
11
- * Resolve caller-provided Graphile configuration before deciding whether the
12
- * optional scoped introspection package is needed. Graphile config applies
13
- * nested `extends` entries before the containing preset, so this preserves the
14
- * same precedence used by the eventual schema build.
6
+ * Resolve caller-provided Graphile configuration with the same `extends`
7
+ * precedence used by the eventual schema build.
15
8
  */
16
9
  export const resolveCallerPreset = (graphileOptions) => {
17
10
  const preset = graphileOptions?.preset;
@@ -21,45 +14,29 @@ export const resolveCallerPreset = (graphileOptions) => {
21
14
  ];
22
15
  return presets.length > 0 ? resolvePreset({ extends: presets }) : undefined;
23
16
  };
24
- const hasLocalScopedIntrospectionPreset = (preset) => preset?.plugins.some((plugin) => plugin.name === SCOPED_INTROSPECTION_PLUGIN) ??
25
- false;
26
- const hasUpstreamIntrospectionDisabled = (preset) => preset?.disablePlugins.includes('PgIntrospectionPlugin') ?? false;
17
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
27
18
  /**
28
- * Select the stock or scoped introspection wiring once, while constructing a
29
- * server-owned schema handler. The stock branch returns before the scoped
30
- * package (and its upstream contract sentinel) is loaded.
19
+ * Every server handler introspects only its API's schemas and their dependency
20
+ * closure. Stock introspection reads the whole database into each cached
21
+ * handler, so it is not an option here: `main: false` is rejected. Caller
22
+ * options for `main` (catalog policy, capability extensions) are kept.
31
23
  */
32
- export const makeIntrospectionWiring = async (pool, schemas, graphileOptions, loadScopedPreset = loadScopedIntrospectionPreset, introspectionRole) => {
24
+ export const makeIntrospectionWiring = (pool, schemas, graphileOptions, introspectionRole) => {
33
25
  const pgSettingsForIntrospection = introspectionRole ? { role: introspectionRole } : undefined;
34
26
  const pgService = makePgService({ pool, schemas, pgSettingsForIntrospection });
35
- const callerPreset = resolveCallerPreset(graphileOptions);
36
- const scopedConfig = callerPreset?.gather?.pgScopedIntrospection;
37
- const hasScopedConfiguration = scopedConfig !== undefined;
38
- // An explicitly supplied local preset already installs the replacement
39
- // plugin. Keep that path synchronous and avoid importing the optional
40
- // package a second time. A raw plugin entry may omit the stock plugin's
41
- // disablement, so add that narrow preset to keep the replacement atomic.
42
- if (hasLocalScopedIntrospectionPreset(callerPreset)) {
43
- return {
44
- presets: hasUpstreamIntrospectionDisabled(callerPreset)
45
- ? []
46
- : [{ disablePlugins: ['PgIntrospectionPlugin'] }],
47
- pgService
48
- };
27
+ const configured = resolveCallerPreset(graphileOptions)?.gather?.pgScopedIntrospection;
28
+ if (configured !== undefined && !isPlainObject(configured)) {
29
+ throw new Error('pgScopedIntrospection must be an object keyed by PostgreSQL service name');
49
30
  }
50
- if (!hasScopedConfiguration) {
51
- return {
52
- presets: [],
53
- pgService
54
- };
31
+ if (configured?.[SERVICE_NAME] === false) {
32
+ throw new Error(`Schema-scoped introspection cannot be disabled for the server's '${SERVICE_NAME}' service: stock introspection retains the whole database in every cached handler`);
55
33
  }
56
- // Load the replacement whenever the map is supplied, including an empty map,
57
- // entries set to false, or entries keyed by an unknown service. The
58
- // replacement owns validation; leaving it out would cause Graphile's stock
59
- // plugin to silently ignore the gather option in those cases.
60
- const scopedPreset = await loadScopedPreset();
61
34
  return {
62
- presets: [scopedPreset],
63
- pgService
35
+ presets: [ScopedIntrospectionPreset],
36
+ pgService,
37
+ pgScopedIntrospection: {
38
+ ...configured,
39
+ [SERVICE_NAME]: configured?.[SERVICE_NAME] ?? true
40
+ }
64
41
  };
65
42
  };
@@ -2,7 +2,7 @@ import './types'; // for Request type
2
2
  import { errors } from '@constructive-io/errors';
3
3
  import { DEFAULT_REQUEST_PROTECTION, protectionPgSettings } from '@constructive-io/express-context';
4
4
  import { Logger } from '@pgpmjs/logger';
5
- import { createGraphileInstance, graphileCache } from 'graphile-cache';
5
+ import { cacheGraphileInstance, createGraphileInstance, graphileCache } from 'graphile-cache';
6
6
  import { createFunctionBindingsPlugin } from 'graphile-function-bindings';
7
7
  import { createConstructivePreset, createGrafastCacheLimitsPreset } from 'graphile-settings';
8
8
  import { getPgPool } from 'pg-cache';
@@ -56,8 +56,8 @@ const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
56
56
  * plugin preset. Without settings the default preset is used
57
57
  * (everything on except aggregates).
58
58
  */
59
- const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn) => {
60
- const introspection = await makeIntrospectionWiring(pool, schemas, graphileOptions, undefined, introspectionRole);
59
+ const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn) => {
60
+ const introspection = makeIntrospectionWiring(pool, schemas, graphileOptions, introspectionRole);
61
61
  const configuredPreset = graphileOptions?.preset ?? {};
62
62
  const grafastCachePreset = createGrafastCacheLimitsPreset(graphileOptions?.grafastCache);
63
63
  return {
@@ -100,6 +100,10 @@ const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole,
100
100
  : [])
101
101
  ],
102
102
  pgServices: [introspection.pgService],
103
+ gather: {
104
+ ...configuredPreset.gather,
105
+ pgScopedIntrospection: introspection.pgScopedIntrospection
106
+ },
103
107
  grafserv: {
104
108
  graphqlPath: '/graphql',
105
109
  graphiqlPath: '/graphiql',
@@ -279,7 +283,7 @@ export const graphile = (opts) => {
279
283
  return instance.handler(req, res, next);
280
284
  }
281
285
  catch (error) {
282
- log.warn(`${label} Coalesced request failed for PostGraphile[${key}], retrying`);
286
+ log.warn(`${label} Coalesced request failed for PostGraphile[${key}], retrying`, error);
283
287
  // Fall through to Phase C to retry creation
284
288
  }
285
289
  }
@@ -311,8 +315,8 @@ export const graphile = (opts) => {
311
315
  // concurrent requests coalesce while optional plugin modules load.
312
316
  const creationPromise = (async () => {
313
317
  const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
314
- const preset = await buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings));
315
- return observeGraphileBuild({
318
+ const preset = buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings));
319
+ const instance = await observeGraphileBuild({
316
320
  cacheKey: key,
317
321
  serviceKey: key,
318
322
  databaseId: api.databaseId ?? null
@@ -321,11 +325,14 @@ export const graphile = (opts) => {
321
325
  cacheKey: key,
322
326
  enableRealtime: api.databaseSettings?.enableRealtime
323
327
  }), { enabled: observabilityEnabled });
328
+ // Admission is part of the shared promise: a handler the cache rejects
329
+ // has been released, so coalesced waiters must not be handed it.
330
+ await cacheGraphileInstance(key, instance);
331
+ return instance;
324
332
  })();
325
333
  creating.set(key, creationPromise);
326
334
  try {
327
335
  const instance = await creationPromise;
328
- graphileCache.set(key, instance);
329
336
  log.info(`${label} Cached PostGraphile v5 handler key=${key} db=${dbname}`);
330
337
  return instance.handler(req, res, next);
331
338
  }
@@ -1,23 +1,22 @@
1
- import type { GraphileOptions } from '@constructive-io/graphql-types';
1
+ import type { GraphileOptions, PgScopedIntrospectionConfig } from '@constructive-io/graphql-types';
2
2
  import { type GraphileConfig } from 'graphile-config';
3
3
  import type { Pool } from 'pg';
4
4
  export interface IntrospectionWiring {
5
5
  presets: GraphileConfig.Preset[];
6
6
  pgService: GraphileConfig.PgServiceConfiguration;
7
+ pgScopedIntrospection: PgScopedIntrospectionConfig;
7
8
  }
8
- export type ScopedIntrospectionPresetLoader = () => Promise<GraphileConfig.Preset>;
9
9
  type ResolvedCallerPreset = GraphileConfig.ResolvedPreset | undefined;
10
10
  /**
11
- * Resolve caller-provided Graphile configuration before deciding whether the
12
- * optional scoped introspection package is needed. Graphile config applies
13
- * nested `extends` entries before the containing preset, so this preserves the
14
- * same precedence used by the eventual schema build.
11
+ * Resolve caller-provided Graphile configuration with the same `extends`
12
+ * precedence used by the eventual schema build.
15
13
  */
16
14
  export declare const resolveCallerPreset: (graphileOptions: GraphileOptions | undefined) => ResolvedCallerPreset;
17
15
  /**
18
- * Select the stock or scoped introspection wiring once, while constructing a
19
- * server-owned schema handler. The stock branch returns before the scoped
20
- * package (and its upstream contract sentinel) is loaded.
16
+ * Every server handler introspects only its API's schemas and their dependency
17
+ * closure. Stock introspection reads the whole database into each cached
18
+ * handler, so it is not an option here: `main: false` is rejected. Caller
19
+ * options for `main` (catalog policy, capability extensions) are kept.
21
20
  */
22
- export declare const makeIntrospectionWiring: (pool: Pool, schemas: string[], graphileOptions: GraphileOptions | undefined, loadScopedPreset?: ScopedIntrospectionPresetLoader, introspectionRole?: string) => Promise<IntrospectionWiring>;
21
+ export declare const makeIntrospectionWiring: (pool: Pool, schemas: string[], graphileOptions: GraphileOptions | undefined, introspectionRole?: string) => IntrospectionWiring;
23
22
  export {};
@@ -2,19 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.makeIntrospectionWiring = exports.resolveCallerPreset = void 0;
4
4
  const graphile_config_1 = require("graphile-config");
5
+ const graphile_scoped_introspection_1 = require("graphile-scoped-introspection");
5
6
  const graphile_settings_1 = require("graphile-settings");
6
- const SCOPED_INTROSPECTION_PLUGIN = 'ConstructivePgIntrospectionPlugin';
7
- let scopedIntrospectionPresetPromise;
8
- const loadScopedIntrospectionPreset = () => {
9
- scopedIntrospectionPresetPromise ??=
10
- import('graphile-scoped-introspection').then(({ ScopedIntrospectionPreset }) => ScopedIntrospectionPreset);
11
- return scopedIntrospectionPresetPromise;
12
- };
7
+ const SERVICE_NAME = 'main';
13
8
  /**
14
- * Resolve caller-provided Graphile configuration before deciding whether the
15
- * optional scoped introspection package is needed. Graphile config applies
16
- * nested `extends` entries before the containing preset, so this preserves the
17
- * same precedence used by the eventual schema build.
9
+ * Resolve caller-provided Graphile configuration with the same `extends`
10
+ * precedence used by the eventual schema build.
18
11
  */
19
12
  const resolveCallerPreset = (graphileOptions) => {
20
13
  const preset = graphileOptions?.preset;
@@ -25,46 +18,30 @@ const resolveCallerPreset = (graphileOptions) => {
25
18
  return presets.length > 0 ? (0, graphile_config_1.resolvePreset)({ extends: presets }) : undefined;
26
19
  };
27
20
  exports.resolveCallerPreset = resolveCallerPreset;
28
- const hasLocalScopedIntrospectionPreset = (preset) => preset?.plugins.some((plugin) => plugin.name === SCOPED_INTROSPECTION_PLUGIN) ??
29
- false;
30
- const hasUpstreamIntrospectionDisabled = (preset) => preset?.disablePlugins.includes('PgIntrospectionPlugin') ?? false;
21
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
31
22
  /**
32
- * Select the stock or scoped introspection wiring once, while constructing a
33
- * server-owned schema handler. The stock branch returns before the scoped
34
- * package (and its upstream contract sentinel) is loaded.
23
+ * Every server handler introspects only its API's schemas and their dependency
24
+ * closure. Stock introspection reads the whole database into each cached
25
+ * handler, so it is not an option here: `main: false` is rejected. Caller
26
+ * options for `main` (catalog policy, capability extensions) are kept.
35
27
  */
36
- const makeIntrospectionWiring = async (pool, schemas, graphileOptions, loadScopedPreset = loadScopedIntrospectionPreset, introspectionRole) => {
28
+ const makeIntrospectionWiring = (pool, schemas, graphileOptions, introspectionRole) => {
37
29
  const pgSettingsForIntrospection = introspectionRole ? { role: introspectionRole } : undefined;
38
30
  const pgService = (0, graphile_settings_1.makePgService)({ pool, schemas, pgSettingsForIntrospection });
39
- const callerPreset = (0, exports.resolveCallerPreset)(graphileOptions);
40
- const scopedConfig = callerPreset?.gather?.pgScopedIntrospection;
41
- const hasScopedConfiguration = scopedConfig !== undefined;
42
- // An explicitly supplied local preset already installs the replacement
43
- // plugin. Keep that path synchronous and avoid importing the optional
44
- // package a second time. A raw plugin entry may omit the stock plugin's
45
- // disablement, so add that narrow preset to keep the replacement atomic.
46
- if (hasLocalScopedIntrospectionPreset(callerPreset)) {
47
- return {
48
- presets: hasUpstreamIntrospectionDisabled(callerPreset)
49
- ? []
50
- : [{ disablePlugins: ['PgIntrospectionPlugin'] }],
51
- pgService
52
- };
31
+ const configured = (0, exports.resolveCallerPreset)(graphileOptions)?.gather?.pgScopedIntrospection;
32
+ if (configured !== undefined && !isPlainObject(configured)) {
33
+ throw new Error('pgScopedIntrospection must be an object keyed by PostgreSQL service name');
53
34
  }
54
- if (!hasScopedConfiguration) {
55
- return {
56
- presets: [],
57
- pgService
58
- };
35
+ if (configured?.[SERVICE_NAME] === false) {
36
+ throw new Error(`Schema-scoped introspection cannot be disabled for the server's '${SERVICE_NAME}' service: stock introspection retains the whole database in every cached handler`);
59
37
  }
60
- // Load the replacement whenever the map is supplied, including an empty map,
61
- // entries set to false, or entries keyed by an unknown service. The
62
- // replacement owns validation; leaving it out would cause Graphile's stock
63
- // plugin to silently ignore the gather option in those cases.
64
- const scopedPreset = await loadScopedPreset();
65
38
  return {
66
- presets: [scopedPreset],
67
- pgService
39
+ presets: [graphile_scoped_introspection_1.ScopedIntrospectionPreset],
40
+ pgService,
41
+ pgScopedIntrospection: {
42
+ ...configured,
43
+ [SERVICE_NAME]: configured?.[SERVICE_NAME] ?? true
44
+ }
68
45
  };
69
46
  };
70
47
  exports.makeIntrospectionWiring = makeIntrospectionWiring;
@@ -62,8 +62,8 @@ const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
62
62
  * plugin preset. Without settings the default preset is used
63
63
  * (everything on except aggregates).
64
64
  */
65
- const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn) => {
66
- const introspection = await (0, graphile_introspection_1.makeIntrospectionWiring)(pool, schemas, graphileOptions, undefined, introspectionRole);
65
+ const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn) => {
66
+ const introspection = (0, graphile_introspection_1.makeIntrospectionWiring)(pool, schemas, graphileOptions, introspectionRole);
67
67
  const configuredPreset = graphileOptions?.preset ?? {};
68
68
  const grafastCachePreset = (0, graphile_settings_1.createGrafastCacheLimitsPreset)(graphileOptions?.grafastCache);
69
69
  return {
@@ -106,6 +106,10 @@ const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole,
106
106
  : [])
107
107
  ],
108
108
  pgServices: [introspection.pgService],
109
+ gather: {
110
+ ...configuredPreset.gather,
111
+ pgScopedIntrospection: introspection.pgScopedIntrospection
112
+ },
109
113
  grafserv: {
110
114
  graphqlPath: '/graphql',
111
115
  graphiqlPath: '/graphiql',
@@ -285,7 +289,7 @@ const graphile = (opts) => {
285
289
  return instance.handler(req, res, next);
286
290
  }
287
291
  catch (error) {
288
- log.warn(`${label} Coalesced request failed for PostGraphile[${key}], retrying`);
292
+ log.warn(`${label} Coalesced request failed for PostGraphile[${key}], retrying`, error);
289
293
  // Fall through to Phase C to retry creation
290
294
  }
291
295
  }
@@ -317,8 +321,8 @@ const graphile = (opts) => {
317
321
  // concurrent requests coalesce while optional plugin modules load.
318
322
  const creationPromise = (async () => {
319
323
  const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
320
- const preset = await buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings));
321
- return (0, graphile_build_stats_1.observeGraphileBuild)({
324
+ const preset = buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings));
325
+ const instance = await (0, graphile_build_stats_1.observeGraphileBuild)({
322
326
  cacheKey: key,
323
327
  serviceKey: key,
324
328
  databaseId: api.databaseId ?? null
@@ -327,11 +331,14 @@ const graphile = (opts) => {
327
331
  cacheKey: key,
328
332
  enableRealtime: api.databaseSettings?.enableRealtime
329
333
  }), { enabled: observabilityEnabled });
334
+ // Admission is part of the shared promise: a handler the cache rejects
335
+ // has been released, so coalesced waiters must not be handed it.
336
+ await (0, graphile_cache_1.cacheGraphileInstance)(key, instance);
337
+ return instance;
330
338
  })();
331
339
  creating.set(key, creationPromise);
332
340
  try {
333
341
  const instance = await creationPromise;
334
- graphile_cache_1.graphileCache.set(key, instance);
335
342
  log.info(`${label} Cached PostGraphile v5 handler key=${key} db=${dbname}`);
336
343
  return instance.handler(req, res, next);
337
344
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-server",
3
- "version": "5.32.1",
3
+ "version": "5.32.2",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Constructive GraphQL Server",
6
6
  "main": "index.js",
@@ -44,7 +44,7 @@
44
44
  "@constructive-io/csrf": "^0.31.0",
45
45
  "@constructive-io/errors": "^0.25.0",
46
46
  "@constructive-io/express-context": "^0.35.0",
47
- "@constructive-io/graphql-env": "^3.37.0",
47
+ "@constructive-io/graphql-env": "^3.37.1",
48
48
  "@constructive-io/graphql-types": "^3.36.0",
49
49
  "@constructive-io/llm-env": "^0.16.0",
50
50
  "@constructive-io/query-builder": "^3.17.0",
@@ -65,13 +65,13 @@
65
65
  "grafserv": "1.0.1",
66
66
  "graphile-build": "5.1.1",
67
67
  "graphile-build-pg": "5.1.3",
68
- "graphile-cache": "^4.16.0",
68
+ "graphile-cache": "^4.16.1",
69
69
  "graphile-config": "1.1.0",
70
- "graphile-function-bindings": "^1.18.0",
70
+ "graphile-function-bindings": "^1.18.1",
71
71
  "graphile-plugin-utils": "^1.14.0",
72
72
  "graphile-presigned-url-plugin": "^1.23.1",
73
73
  "graphile-scoped-introspection": "^0.5.0",
74
- "graphile-settings": "^6.26.1",
74
+ "graphile-settings": "^6.26.2",
75
75
  "graphile-utils": "5.0.3",
76
76
  "graphql": "16.13.0",
77
77
  "graphql-upload": "^13.0.0",
@@ -94,11 +94,11 @@
94
94
  "@types/request-ip": "^0.0.41",
95
95
  "@types/supertest": "^7.2.1",
96
96
  "cookie-parser": "^1.4.7",
97
- "graphile-test": "5.18.0",
97
+ "graphile-test": "5.18.1",
98
98
  "makage": "^0.8.0",
99
99
  "nodemon": "^3.1.14",
100
100
  "supertest": "^7.2.2",
101
101
  "ts-node": "^10.9.2"
102
102
  },
103
- "gitHead": "6cbaa8605270a2e019439fd9f444a35b3f2a9de0"
103
+ "gitHead": "91e18cc80da92331fb3aadd5baa43251c8934bc8"
104
104
  }