Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .changeset/scoped-introspection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"graphile-build-pg": minor
"pg-introspection": minor
---

Add opt-in schema-scoped PostgreSQL introspection with transitive dependency
closure and fail-closed schema boundary validation.
33 changes: 33 additions & 0 deletions graphile-build/graphile-build-pg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@ creates the relevant GraphQL types, fields, and [grafast][] plan resolver
functions. The result is a high-performance, powerful, auto-generated but highly
flexible GraphQL schema.

## Schema-scoped introspection

PostgreSQL services can opt into schema-scoped introspection through gather
options keyed by service name. Services without an entry continue to use the
full catalog query.

```ts
const preset = {
pgServices: [
makePgService({
name: "main",
connectionString: process.env.DATABASE_URL,
schemas: ["app_public"],
}),
],
gather: {
pgScopedIntrospection: {
main: {
allowedDependencySchemas: ["app_private"],
catalogTypes: "dependency-closure" as const,
capabilityExtensions: ["pg_trgm"],
},
},
},
};
```

The service's `schemas` are the roots of the introspection query. Referenced
objects in other schemas are retained only when those schemas are listed in
`allowedDependencySchemas`; an unapproved crossing fails schema construction.
Configuration for an unknown service name also fails rather than being silently
ignored.

If you don't want to use your database introspection results to generate the
schema, you can instead build the registry yourself giving you full control over
what goes into your GraphQL API whilst still saving you significant effort
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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
);
Loading