Skip to content

Add opt-in CNC scoped introspection - #1719

Open
Zetazzz wants to merge 7 commits into
test/performance-harness-corefrom
feat/cnc-scoped-introspection
Open

Add opt-in CNC scoped introspection#1719
Zetazzz wants to merge 7 commits into
test/performance-harness-corefrom
feat/cnc-scoped-introspection

Conversation

@Zetazzz

@Zetazzz Zetazzz commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add the independently publishable graphile-scoped-introspection package under graphile/.
  • Keep the Constructive server on untouched upstream Graphile introspection by default.
  • Add the strictly validated startup switch GRAPHILE_INTROSPECTION_MODE=stock|scoped-required; absence defaults to stock.
  • Load the scoped replacement preset and apply scoped-only PgService settings only when scoped-required is selected.
  • Keep the stock/scoped fresh-process performance comparison on the same explicit package boundary.
  • Pass the benchmark database URL and opaque worker configuration through the core harness CLI protocol rather than perf-only environment variables.

Default and opt-in boundaries

stock (default)

  • ConstructivePreset retains upstream PgIntrospectionPlugin.
  • graphile-settings.makePgService is the upstream PostGraphile factory, without CNC service fields or introspection settings.
  • The server does not load graphile-scoped-introspection, so its pinned upstream contract sentinel cannot affect stock startup.

scoped-required (opt-in)

  • The server dynamically loads ScopedIntrospectionPreset while constructing the schema handler.
  • makeScopedPgService selects dependency-closure catalog retention, bounded introspection settings, configured dependency schemas, and configured capability extensions.
  • Missing required schemas, unapproved namespace crossings, incomplete type closure, and upstream contract drift remain fail closed.

Configuration is owned by @constructive-io/graphql-env and forwarded through typed GraphileOptions; the plugin package does not read process.env. Precedence remains defaults → config file → environment → runtime override, and malformed explicit mode values fail during option resolution.

Architecture

The scoped SQL is CNC-owned, static, and parameterized. It is adapted from the MIT-licensed pg-introspection@1.0.1 query structure and does not patch installed Graphile, Dataplan, or pg-introspection packages or import private package subpaths.

The introspection mode, catalog policy, and PgService option contracts have a single owner in @constructive-io/graphql-types. The scoped plugin re-exports those public types for compatibility; graphile-settings consumes the canonical contract directly and has no package dependency on graphile-scoped-introspection.

Database clients continue to use the normal three-argument withPgClientFromPgService(service, settings, callback) lifecycle. Final pool teardown remains the responsibility of PgService.release().

Validation

  • @constructive-io/graphql-env: 1 suite / 14 tests passed, including default, valid values, malformed values, and config/env/runtime precedence.
  • graphile-settings: 6 non-database suites / 47 tests passed, including upstream-default and scoped service wiring.
  • @constructive-io/graphql-server: 14 suites / 145 tests passed, including proof that stock wiring does not invoke the scoped loader.
  • graphile-scoped-introspection: 6 suites / 18 tests passed.
  • @constructive-io/perf-harness: 6 suites / 11 tests passed, including the explicit worker CLI protocol.
  • CJS and ESM builds passed for all six affected packages.
  • Affected ESLint completed with zero errors (one pre-existing warning).
  • Final PR-range regular and whitespace-ignoring diff statistics are identical; git diff --check against the PR base passed.
  • pnpm install --frozen-lockfile and offline frozen-lockfile install passed.
  • Compiled CJS and ESM server output retains a dynamic import, and compiled graphile-settings has no runtime reference to the scoped package.
  • PostgreSQL 18.4 fresh-process smoke passed with distinct PIDs (28375, 28378), matching schema hash 67dae5a669c8aaa4a8cbf71a943cc0854f8ad610e835c03204c9e178c2cbe5f3, successful runtime validation in both cases, and no database URL in the report.

Follow-up

When Graphile progressive introspection is available, CNC can replace this concentrated opt-in package boundary with the upstream implementation while preserving the server configuration boundary.

@Zetazzz
Zetazzz force-pushed the feat/cnc-scoped-introspection branch from d42cd85 to 8a83d70 Compare August 14, 2026 13:09
@Zetazzz Zetazzz changed the title Add CNC-owned scoped PostgreSQL introspection Add zero-patch CNC scoped introspection package Aug 14, 2026
@Zetazzz Zetazzz changed the title Add zero-patch CNC scoped introspection package Add opt-in CNC scoped introspection Aug 18, 2026
@Zetazzz
Zetazzz force-pushed the feat/cnc-scoped-introspection branch from a8fed57 to fa39446 Compare August 18, 2026 05:19
@Zetazzz
Zetazzz force-pushed the test/performance-harness-core branch from 45cbb07 to 658736e Compare September 6, 2026 01:51
@Zetazzz
Zetazzz force-pushed the feat/cnc-scoped-introspection branch from fa39446 to 0e14abd Compare September 6, 2026 02:08
@Zetazzz
Zetazzz force-pushed the test/performance-harness-core branch from 658736e to f7c63ec Compare September 7, 2026 03:22
@Zetazzz
Zetazzz force-pushed the feat/cnc-scoped-introspection branch from 0e14abd to 8a95026 Compare September 7, 2026 05:59
@Zetazzz
Zetazzz marked this pull request as ready for review September 7, 2026 05:59
@tenki-reviewer

tenki-reviewer Bot commented Sep 7, 2026

Copy link
Copy Markdown

Review complete. 🟡 3 medium

💬 Inline comments (2)

📍 Findings outside the diff (1) — 🟡 1 medium — defects on lines GitHub can't attach comments to

🟡 Medium — Await buildPreset before registering in-flight promise · graphile.ts:351–375 · unchanged line

// graphql/server/src/middleware/graphile.ts
351	      const preset = await buildPreset(
352	        pool,
353	        schema || [],
354	        anonRole,
355	        roleName,
356	        opts.api?.introspectionRole,
357	        opts.graphile,
358	        api.databaseSettings,
359	        api.apiId,
360	        compute
361	      );
362	      const creationPromise = observeGraphileBuild(

buildPreset became async and is now awaited at graphile.ts:351 before creating.set(key, creationPromise) at graphile.ts:375, so the single-flight map is populated only after an asynchronous gap that did not exist for the common no-apiId path. Two concurrent cache-miss requests for the same key can both pass the cache and in-flight checks, each build a handler, and the later overwrites the earlier in-flight entry — duplicate, expensive handler creation that defeats the documented single-flight invariant.

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 Peer range allows versions the plugin rejects (plugin.ts:65) — plugin.ts throws at module load unless the installed graphile-build-pg version is exactly 5.1.3 (plugin.ts:65), but package.json declares the peer dependency as ^5.1.3 (package.json:43), which permits later 5.1.x.

This PR introduces a scoped-introspection feature that restricts the PostGraphile catalog to an app's dependency closure, plus a parameterized SQL query builder, a preset plugin, server middleware, and a perf harness. The feature is well-tested and the SQL is parameterized against injection.

Files Change
graphile/graphile-scoped-introspection New package: scoped introspection query builder, plugin, and re-exports
graphile/graphile-settings Wiring/service for scoped introspection settings and catalog types
graphql/server/src/middleware Async preset build and scoped introspection middleware wiring
graphql/env + graphql/types Env merge and type/docs for the new scoped catalog option
packages/perf-harness Scoped-introspection benchmark suite and worker

Four findings: a documented-default mismatch for the scoped catalog type, capability-extension namespaces not approved in allowedSchemas, a single-flight race from awaiting buildPreset, and a peer-range/version-check mismatch. The sweep pass did not publish its artifact; its findings were folded into the adjudicated set.

Reviewed commit: 8a95026

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds schema-scoped PostgreSQL catalog introspection (graphile-scoped-introspection) with a new scoped query builder, preset plugin, wiring, server middleware, and perf harness, surfaced through graphile-settings and graphql/env.

Key findings

Comment on lines +87 to +88
introspectionScopedCatalogTypes:
introspectionScopedCatalogTypes ?? 'dependency-closure',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

Default scoped catalog type contradicts documented 'all'

makeScopedPgService defaults introspectionScopedCatalogTypes to 'dependency-closure' (scoped-introspection-service.ts:88), while the ScopedIntrospectionServiceOptions contract documents the default as 'all' (graphql/types/src/graphile.ts:13). A consumer calling the exported factory without the option silently retains only the dependency closure instead of the documented full catalog, dropping schema types with no error.

📋 Prompt for AI Agents

In graphile/graphile-settings/src/scoped-introspection-service.ts lines 87-88, the fallback introspectionScopedCatalogTypes ?? 'dependency-closure' contradicts the documented default of 'all' in graphql/types/src/graphile.ts line 13. Decide the intended default: if 'all' is correct, change the fallback to ?? 'all' and update graphile/graphile-scoped-introspection/src/plugin.ts line 92 and graphql/server/src/middleware/graphile-introspection.ts line 64 accordingly; otherwise update the doc comment in graphql/types/src/graphile.ts to state the actual 'dependency-closure' default so the public contract matches the implementation.

Comment on lines +130 to +132
allowedSchemas: [
...new Set([...requiredSchemas, ...dependencySchemas, 'pg_catalog']),
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

Approve capability-extension namespaces in allowedSchemas

allowedSchemas is built from requiredSchemas, dependency schemas, and pg_catalog only (plugin.ts:130), but the scoped SQL adds installed_extensions.extnamespace for every requested capability extension. When an extension such as pg_trgm lives in a schema outside that set (the default: extensions in public, app introspecting tenant_a), assertScopedNamespaces throws "crossed into unapproved dependency schema(s)" and aborts the schema build for an explicitly requested feature.

📋 Prompt for AI Agents

In graphile/graphile-scoped-introspection/src/plugin.ts around lines 120-134, the allowedSchemas array for scoped-required mode is built from requiredSchemas, dependencySchemas, and 'pg_catalog' only. Because the generated query's scoped_namespaces CTE (scoped-introspection-query.ts:349-351) includes installed_extensions.extnamespace for every requested capability extension, an extension installed in a schema outside that allowed set (e.g. pg_trgm in public while the app introspects tenant_a) causes assertScopedNamespaces (plugin.ts:159) to throw and abort schema building. Fix: resolve each extension in introspectionCapabilityExtensions to its extnamespace and add those namespaces to allowedSchemas before the assertion, so explicitly requested capability extensions do not fail closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants