@constructive-io/graphql-server 5.25.2 → 5.27.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.
@@ -0,0 +1,65 @@
1
+ import { resolvePreset } from 'graphile-config';
2
+ 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
+ };
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.
15
+ */
16
+ export const resolveCallerPreset = (graphileOptions) => {
17
+ const preset = graphileOptions?.preset;
18
+ const presets = [
19
+ ...(graphileOptions?.extends ?? []),
20
+ ...(preset ? [preset] : [])
21
+ ];
22
+ return presets.length > 0 ? resolvePreset({ extends: presets }) : undefined;
23
+ };
24
+ const hasLocalScopedIntrospectionPreset = (preset) => preset?.plugins.some((plugin) => plugin.name === SCOPED_INTROSPECTION_PLUGIN) ??
25
+ false;
26
+ const hasUpstreamIntrospectionDisabled = (preset) => preset?.disablePlugins.includes('PgIntrospectionPlugin') ?? false;
27
+ /**
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.
31
+ */
32
+ export const makeIntrospectionWiring = async (pool, schemas, graphileOptions, loadScopedPreset = loadScopedIntrospectionPreset, introspectionRole) => {
33
+ const pgSettingsForIntrospection = introspectionRole ? { role: introspectionRole } : undefined;
34
+ 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
+ };
49
+ }
50
+ if (!hasScopedConfiguration) {
51
+ return {
52
+ presets: [],
53
+ pgService
54
+ };
55
+ }
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
+ return {
62
+ presets: [scopedPreset],
63
+ pgService
64
+ };
65
+ };
@@ -5,7 +5,7 @@ import { getNodeEnv } from '@pgpmjs/env';
5
5
  import { Logger } from '@pgpmjs/logger';
6
6
  import { createGraphileInstance, graphileCache } from 'graphile-cache';
7
7
  import { createFunctionBindingsPlugin } from 'graphile-function-bindings';
8
- import { createConstructivePreset, makePgService } from 'graphile-settings';
8
+ import { createConstructivePreset } from 'graphile-settings';
9
9
  import { getPgPool } from 'pg-cache';
10
10
  import { getPgEnvOptions } from 'pg-env';
11
11
  import { isGraphqlObservabilityEnabled } from '../diagnostics/observability';
@@ -14,6 +14,7 @@ import { respondWithGraphQLError } from '../errors/graphql-response';
14
14
  import { AuthCookiePlugin } from '../plugins/auth-cookie-plugin';
15
15
  import { createErrorEventsPlugin } from '../plugins/error-events-plugin';
16
16
  import { RequestProtectionPlugin } from '../plugins/request-protection-plugin';
17
+ import { makeIntrospectionWiring } from './graphile-introspection';
17
18
  import { maskError } from './mask-error';
18
19
  import { observeGraphileBuild } from './observability/graphile-build-stats';
19
20
  const isDev = () => getNodeEnv() === 'development';
@@ -56,10 +57,19 @@ const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
56
57
  * plugin preset. Without settings the default preset is used
57
58
  * (everything on except aggregates).
58
59
  */
59
- const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, databaseSettings, apiId, compute) => {
60
+ const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute) => {
61
+ const introspection = await makeIntrospectionWiring(pool, schemas, graphileOptions, undefined, introspectionRole);
62
+ const configuredPreset = graphileOptions?.preset ?? {};
60
63
  return {
61
- extends: [createConstructivePreset(databaseSettings)],
64
+ ...configuredPreset,
65
+ extends: [
66
+ createConstructivePreset(databaseSettings),
67
+ ...(graphileOptions?.extends ?? []),
68
+ ...(configuredPreset.extends ?? []),
69
+ ...introspection.presets
70
+ ],
62
71
  plugins: [
72
+ ...(configuredPreset.plugins ?? []),
63
73
  AuthCookiePlugin,
64
74
  RequestProtectionPlugin,
65
75
  createErrorEventsPlugin(pool),
@@ -83,20 +93,7 @@ const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, datab
83
93
  ]
84
94
  : [])
85
95
  ],
86
- pgServices: [
87
- makePgService({
88
- pool,
89
- schemas,
90
- // Introspection runs outside any request, so it has no served role to
91
- // inherit: unset, it reads the catalog as whatever role the pool
92
- // connected as (a superuser in most deployments) and the schema
93
- // advertises that role's reach. Naming the role keeps schema shape
94
- // tied to a bounded role's grants.
95
- ...(introspectionRole && {
96
- pgSettingsForIntrospection: { role: introspectionRole }
97
- })
98
- })
99
- ],
96
+ pgServices: [introspection.pgService],
100
97
  grafserv: {
101
98
  graphqlPath: '/graphql',
102
99
  graphiqlPath: '/graphiql',
@@ -304,18 +301,21 @@ export const graphile = (opts) => {
304
301
  // Route through pg-cache so the pool is tracked and can be cleaned up
305
302
  // properly, preventing leaked connections during database teardown.
306
303
  const pool = getPgPool(pgConfig);
307
- // Create promise and store in in-flight map BEFORE try block
308
- const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
309
- const preset = buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, api.databaseSettings, api.apiId, compute);
310
- const creationPromise = observeGraphileBuild({
311
- cacheKey: key,
312
- serviceKey: key,
313
- databaseId: api.databaseId ?? null
314
- }, () => createGraphileInstance({
315
- preset,
316
- cacheKey: key,
317
- enableRealtime: api.databaseSettings?.enableRealtime
318
- }), { enabled: observabilityEnabled });
304
+ // Register the creation promise before any asynchronous preset work so
305
+ // concurrent requests coalesce while optional plugin modules load.
306
+ const creationPromise = (async () => {
307
+ const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
308
+ const preset = await buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute);
309
+ return observeGraphileBuild({
310
+ cacheKey: key,
311
+ serviceKey: key,
312
+ databaseId: api.databaseId ?? null
313
+ }, () => createGraphileInstance({
314
+ preset,
315
+ cacheKey: key,
316
+ enableRealtime: api.databaseSettings?.enableRealtime
317
+ }), { enabled: observabilityEnabled });
318
+ })();
319
319
  creating.set(key, creationPromise);
320
320
  try {
321
321
  const instance = await creationPromise;
@@ -0,0 +1,23 @@
1
+ import type { GraphileOptions } from '@constructive-io/graphql-types';
2
+ import { type GraphileConfig } from 'graphile-config';
3
+ import type { Pool } from 'pg';
4
+ export interface IntrospectionWiring {
5
+ presets: GraphileConfig.Preset[];
6
+ pgService: GraphileConfig.PgServiceConfiguration;
7
+ }
8
+ export type ScopedIntrospectionPresetLoader = () => Promise<GraphileConfig.Preset>;
9
+ type ResolvedCallerPreset = GraphileConfig.ResolvedPreset | undefined;
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.
15
+ */
16
+ export declare const resolveCallerPreset: (graphileOptions: GraphileOptions | undefined) => ResolvedCallerPreset;
17
+ /**
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.
21
+ */
22
+ export declare const makeIntrospectionWiring: (pool: Pool, schemas: string[], graphileOptions: GraphileOptions | undefined, loadScopedPreset?: ScopedIntrospectionPresetLoader, introspectionRole?: string) => Promise<IntrospectionWiring>;
23
+ export {};
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeIntrospectionWiring = exports.resolveCallerPreset = void 0;
4
+ const graphile_config_1 = require("graphile-config");
5
+ 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
+ };
13
+ /**
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.
18
+ */
19
+ const resolveCallerPreset = (graphileOptions) => {
20
+ const preset = graphileOptions?.preset;
21
+ const presets = [
22
+ ...(graphileOptions?.extends ?? []),
23
+ ...(preset ? [preset] : [])
24
+ ];
25
+ return presets.length > 0 ? (0, graphile_config_1.resolvePreset)({ extends: presets }) : undefined;
26
+ };
27
+ 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;
31
+ /**
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.
35
+ */
36
+ const makeIntrospectionWiring = async (pool, schemas, graphileOptions, loadScopedPreset = loadScopedIntrospectionPreset, introspectionRole) => {
37
+ const pgSettingsForIntrospection = introspectionRole ? { role: introspectionRole } : undefined;
38
+ 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
+ };
53
+ }
54
+ if (!hasScopedConfiguration) {
55
+ return {
56
+ presets: [],
57
+ pgService
58
+ };
59
+ }
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
+ return {
66
+ presets: [scopedPreset],
67
+ pgService
68
+ };
69
+ };
70
+ exports.makeIntrospectionWiring = makeIntrospectionWiring;
@@ -20,6 +20,7 @@ const graphql_response_1 = require("../errors/graphql-response");
20
20
  const auth_cookie_plugin_1 = require("../plugins/auth-cookie-plugin");
21
21
  const error_events_plugin_1 = require("../plugins/error-events-plugin");
22
22
  const request_protection_plugin_1 = require("../plugins/request-protection-plugin");
23
+ const graphile_introspection_1 = require("./graphile-introspection");
23
24
  const mask_error_1 = require("./mask-error");
24
25
  const graphile_build_stats_1 = require("./observability/graphile-build-stats");
25
26
  const isDev = () => (0, env_1.getNodeEnv)() === 'development';
@@ -62,10 +63,19 @@ const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
62
63
  * plugin preset. Without settings the default preset is used
63
64
  * (everything on except aggregates).
64
65
  */
65
- const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, databaseSettings, apiId, compute) => {
66
+ const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute) => {
67
+ const introspection = await (0, graphile_introspection_1.makeIntrospectionWiring)(pool, schemas, graphileOptions, undefined, introspectionRole);
68
+ const configuredPreset = graphileOptions?.preset ?? {};
66
69
  return {
67
- extends: [(0, graphile_settings_1.createConstructivePreset)(databaseSettings)],
70
+ ...configuredPreset,
71
+ extends: [
72
+ (0, graphile_settings_1.createConstructivePreset)(databaseSettings),
73
+ ...(graphileOptions?.extends ?? []),
74
+ ...(configuredPreset.extends ?? []),
75
+ ...introspection.presets
76
+ ],
68
77
  plugins: [
78
+ ...(configuredPreset.plugins ?? []),
69
79
  auth_cookie_plugin_1.AuthCookiePlugin,
70
80
  request_protection_plugin_1.RequestProtectionPlugin,
71
81
  (0, error_events_plugin_1.createErrorEventsPlugin)(pool),
@@ -89,20 +99,7 @@ const buildPreset = (pool, schemas, anonRole, roleName, introspectionRole, datab
89
99
  ]
90
100
  : [])
91
101
  ],
92
- pgServices: [
93
- (0, graphile_settings_1.makePgService)({
94
- pool,
95
- schemas,
96
- // Introspection runs outside any request, so it has no served role to
97
- // inherit: unset, it reads the catalog as whatever role the pool
98
- // connected as (a superuser in most deployments) and the schema
99
- // advertises that role's reach. Naming the role keeps schema shape
100
- // tied to a bounded role's grants.
101
- ...(introspectionRole && {
102
- pgSettingsForIntrospection: { role: introspectionRole }
103
- })
104
- })
105
- ],
102
+ pgServices: [introspection.pgService],
106
103
  grafserv: {
107
104
  graphqlPath: '/graphql',
108
105
  graphiqlPath: '/graphiql',
@@ -310,18 +307,21 @@ const graphile = (opts) => {
310
307
  // Route through pg-cache so the pool is tracked and can be cleaned up
311
308
  // properly, preventing leaked connections during database teardown.
312
309
  const pool = (0, pg_cache_1.getPgPool)(pgConfig);
313
- // Create promise and store in in-flight map BEFORE try block
314
- const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
315
- const preset = buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, api.databaseSettings, api.apiId, compute);
316
- const creationPromise = (0, graphile_build_stats_1.observeGraphileBuild)({
317
- cacheKey: key,
318
- serviceKey: key,
319
- databaseId: api.databaseId ?? null
320
- }, () => (0, graphile_cache_1.createGraphileInstance)({
321
- preset,
322
- cacheKey: key,
323
- enableRealtime: api.databaseSettings?.enableRealtime
324
- }), { enabled: observabilityEnabled });
310
+ // Register the creation promise before any asynchronous preset work so
311
+ // concurrent requests coalesce while optional plugin modules load.
312
+ const creationPromise = (async () => {
313
+ 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);
315
+ return (0, graphile_build_stats_1.observeGraphileBuild)({
316
+ cacheKey: key,
317
+ serviceKey: key,
318
+ databaseId: api.databaseId ?? null
319
+ }, () => (0, graphile_cache_1.createGraphileInstance)({
320
+ preset,
321
+ cacheKey: key,
322
+ enableRealtime: api.databaseSettings?.enableRealtime
323
+ }), { enabled: observabilityEnabled });
324
+ })();
325
325
  creating.set(key, creationPromise);
326
326
  try {
327
327
  const instance = await creationPromise;
@@ -1,17 +1,5 @@
1
- import type { RequestProtection } from '@constructive-io/express-context';
2
- import type { ApiStructure } from '../types';
3
- export type ConstructiveAPIToken = {
4
- id?: string;
5
- user_id?: string;
6
- principal_id?: string;
7
- session_id?: string;
8
- access_level?: string;
9
- kind?: string;
10
- root_session_id?: string;
11
- parent_session_id?: string;
12
- intent?: string;
13
- [key: string]: unknown;
14
- };
1
+ import type { ApiStructure, ConstructiveAPIToken, RequestProtection } from '@constructive-io/express-context';
2
+ export type { ConstructiveAPIToken } from '@constructive-io/express-context';
15
3
  declare global {
16
4
  namespace Express {
17
5
  interface Request {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-server",
3
- "version": "5.25.2",
3
+ "version": "5.27.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Constructive GraphQL Server",
6
6
  "main": "index.js",
@@ -41,41 +41,42 @@
41
41
  "backend"
42
42
  ],
43
43
  "dependencies": {
44
- "@agentic-kit/ollama": "2.14.0",
45
- "@constructive-io/csrf": "^0.29.1",
46
- "@constructive-io/errors": "^0.16.1",
47
- "@constructive-io/express-context": "^0.31.1",
48
- "@constructive-io/graphql-env": "^3.32.2",
49
- "@constructive-io/graphql-types": "^3.31.2",
50
- "@constructive-io/llm-env": "^0.14.1",
51
- "@constructive-io/query-builder": "^3.13.7",
52
- "@constructive-io/s3-utils": "^2.33.1",
53
- "@constructive-io/url-domains": "^2.30.1",
44
+ "@agentic-kit/ollama": "2.15.0",
45
+ "@constructive-io/csrf": "^0.30.0",
46
+ "@constructive-io/errors": "^0.18.0",
47
+ "@constructive-io/express-context": "^0.33.0",
48
+ "@constructive-io/graphql-env": "^3.34.0",
49
+ "@constructive-io/graphql-types": "^3.33.0",
50
+ "@constructive-io/llm-env": "^0.15.0",
51
+ "@constructive-io/query-builder": "^3.15.0",
52
+ "@constructive-io/s3-utils": "^2.34.0",
53
+ "@constructive-io/url-domains": "^2.31.0",
54
54
  "@graphile-contrib/pg-many-to-many": "2.0.0-rc.2",
55
- "@pgpmjs/env": "^2.43.6",
56
- "@pgpmjs/logger": "^2.25.1",
57
- "@pgpmjs/server-utils": "^3.27.6",
58
- "@pgpmjs/types": "^2.53.4",
55
+ "@pgpmjs/env": "^2.45.0",
56
+ "@pgpmjs/logger": "^2.26.0",
57
+ "@pgpmjs/server-utils": "^3.29.0",
58
+ "@pgpmjs/types": "^2.55.0",
59
59
  "cors": "^2.8.6",
60
60
  "deepmerge": "^4.3.1",
61
61
  "express": "^5.2.1",
62
- "gql-ast": "^3.24.1",
62
+ "gql-ast": "^3.25.0",
63
63
  "grafast": "1.1.2",
64
64
  "grafserv": "1.0.1",
65
65
  "graphile-build": "5.1.1",
66
66
  "graphile-build-pg": "5.1.3",
67
- "graphile-cache": "^4.12.6",
67
+ "graphile-cache": "^4.14.0",
68
68
  "graphile-config": "1.1.0",
69
- "graphile-function-bindings": "^1.14.7",
70
- "graphile-settings": "^6.21.9",
69
+ "graphile-function-bindings": "^1.16.0",
70
+ "graphile-scoped-introspection": "^0.3.0",
71
+ "graphile-settings": "^6.23.0",
71
72
  "graphile-utils": "5.0.3",
72
73
  "graphql": "16.13.0",
73
74
  "graphql-upload": "^13.0.0",
74
75
  "lru-cache": "^11.2.7",
75
76
  "pg": "^8.21.0",
76
- "pg-cache": "^3.27.6",
77
- "pg-env": "^1.31.1",
78
- "pg-query-context": "^2.30.1",
77
+ "pg-cache": "^3.29.0",
78
+ "pg-env": "^1.32.0",
79
+ "pg-query-context": "^2.31.0",
79
80
  "pg-sql2": "5.0.1",
80
81
  "postgraphile": "5.1.4",
81
82
  "request-ip": "^3.3.0"
@@ -90,11 +91,11 @@
90
91
  "@types/request-ip": "^0.0.41",
91
92
  "@types/supertest": "^7.2.1",
92
93
  "cookie-parser": "^1.4.7",
93
- "graphile-test": "5.14.7",
94
+ "graphile-test": "5.16.0",
94
95
  "makage": "^0.8.0",
95
96
  "nodemon": "^3.1.14",
96
97
  "supertest": "^7.2.2",
97
98
  "ts-node": "^10.9.2"
98
99
  },
99
- "gitHead": "9bba983b38395277e0400294aaa380e586b049d4"
100
+ "gitHead": "baa84094b8541eb5000701b0060aa5b298032dc7"
100
101
  }