Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ jobs:
- batch: pg-core
packages: 'pgpm/ast pgpm/traverse pgpm/bundle pgpm/core pgpm/cli pgpm/portability pgpm/export packages/client packages/safegres postgres/pg-codegen postgres/query-builder'
- batch: pg-postgres
packages: 'postgres/pgsql-test postgres/drizzle-orm-test postgres/introspectron graphile/graphile-test graphile/graphile-connection-filter graphile/graphile-postgis'
packages: 'postgres/pgsql-test postgres/drizzle-orm-test postgres/introspectron graphile/graphile-test graphile/graphile-scoped-introspection graphile/graphile-connection-filter graphile/graphile-postgis'
- batch: pg-graphql
packages: 'graphile/graphile-search graphile/graphile-ltree graphile/graphile-bulk-mutations graphile/graphile-function-bindings graphile/graphile-history graphile/graphile-meta graphile/graphile-schema graphql/orm-test graphql/test graphql/playwright-test'
- batch: pg-graphile-extras
Expand Down
67 changes: 67 additions & 0 deletions graphile/graphile-scoped-introspection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# graphile-scoped-introspection

An opt-in Graphile plugin that scopes PostgreSQL catalog introspection to the
configured service schemas and their required dependency closure.

```ts
import { ScopedIntrospectionPreset } from 'graphile-scoped-introspection';

const preset = {
extends: [ScopedIntrospectionPreset],
gather: {
pgScopedIntrospection: {
main: true,
},
},
pgServices: [
{
// standard Graphile PgService fields
name: 'main',
schemas: ['app_public'],
},
],
};
```

With `main: true`, the scoped query uses `catalogTypes: 'all'`: it retains the
dependency closure needed by the selected schemas and all `pg_catalog` types.
It does not expose every user schema or every catalog object. Services omitted
from `gather.pgScopedIntrospection`, or mapped to `false`, use stock
introspection.

Use an options object when the dependency-only policy or extension capability
metadata is needed:

```ts
gather: {
pgScopedIntrospection: {
main: {
catalogTypes: 'dependency-closure',
capabilityExtensions: ['pg_trgm'],
},
},
},
```

The query follows real PostgreSQL dependencies across schemas automatically;
there is no dependency-schema allowlist. `capabilityExtensions` records the
requested extension capability metadata and does not add user schemas to the
scope. Unknown service names fail validation even when mapped to `false`.

The package atomically replaces `PgIntrospectionPlugin` only when its preset
is installed. `true` enables scoped introspection with defaults; an options
object selects the catalog type policy and optional extension capability
metadata.

The scoped SQL is CNC-owned and parameterized. It is adapted from the MIT
licensed `pg-introspection@1.0.1` query and does not patch, import private
subpaths from, or rewrite the installed upstream package.

Use `makeSchemaScopedIntrospectionPlan` when the query result will be parsed
and validated. It returns the normalized schema and option scope alongside the
parameterized query; `makeSchemaScopedIntrospectionQuery` remains available for
callers that only need the SQL.

Database clients use the normal `@dataplan/pg` checkout lifecycle and return
to the pool after each query. Applications remain responsible for calling
`PgService.release()` during final shutdown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
create schema scope_root;
create schema scope_dependency;
create schema scope_unrelated;
create schema scope_extension;
create schema scope_capability_root;

create extension pg_trgm with schema scope_extension;

create type scope_dependency.item_status as enum (
'draft',
'active',
'archived'
);

create domain scope_dependency.positive_integer as integer
check (value > 0);

create type scope_dependency.item_payload as (
status scope_dependency.item_status,
score scope_dependency.positive_integer
);

create type scope_dependency.integer_span as range (
subtype = integer,
multirange_type_name = scope_dependency.integer_span_set
);

create table scope_dependency.dependency_owners (
id bigint generated always as identity primary key,
status scope_dependency.item_status not null
);

create table scope_dependency.inherited_base (
inherited_status scope_dependency.item_status not null
);

create table scope_root.closure_items (
id bigint generated always as identity primary key,
dependency_owner_id bigint not null
references scope_dependency.dependency_owners (id),
title text not null,
status scope_dependency.item_status not null,
score scope_dependency.positive_integer not null,
payload scope_dependency.item_payload not null,
active_span scope_dependency.integer_span
);

create table scope_root.inherited_items (
id bigint generated always as identity primary key
) inherits (scope_dependency.inherited_base);

create table scope_root.inheritance_root (
id bigint generated always as identity primary key,
root_note text not null
);

create table scope_dependency.reverse_inherited_item (
dependency_note text not null
) inherits (scope_root.inheritance_root);

create index closure_items_status_idx
on scope_root.closure_items (status);

create index closure_items_title_gin_trgm_idx
on scope_root.closure_items
using gin (title scope_extension.gin_trgm_ops);

create index closure_items_title_gist_trgm_idx
on scope_root.closure_items
using gist (title scope_extension.gist_trgm_ops(siglen = 32));

create function scope_root.echo_dependency_status(
input_status scope_dependency.item_status
)
returns scope_dependency.item_status
language sql
immutable
strict
parallel safe
as $$
select input_status;
$$;

create function scope_root.make_dependency_payload(
input_status scope_dependency.item_status,
input_score scope_dependency.positive_integer
)
returns scope_dependency.item_payload
language sql
immutable
strict
parallel safe
as $$
select row(input_status, input_score)::scope_dependency.item_payload;
$$;

create type scope_unrelated.item_status as enum (
'draft',
'active',
'archived'
);

create table scope_unrelated.closure_items (
id bigint generated always as identity primary key,
status scope_unrelated.item_status not null
);

create function scope_unrelated.echo_dependency_status(
input_status scope_unrelated.item_status
)
returns scope_unrelated.item_status
language sql
immutable
strict
parallel safe
as $$
select input_status;
$$;

create table scope_capability_root.capability_items (
id bigint generated always as identity primary key,
title text not null
);

insert into scope_dependency.dependency_owners (status)
values ('active');

insert into scope_root.closure_items (
dependency_owner_id,
title,
status,
score,
payload
)
values (
1,
'scoped fixture item',
'active',
7,
row('active', 7)::scope_dependency.item_payload
);
Loading
Loading