Skip to content

Commit 7f2adae

Browse files
committed
feat(gb-9000): provide configuration to enable/disable queries or mutations
1 parent 7c8676f commit 7f2adae

20 files changed

Lines changed: 28483 additions & 4500 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/postgres/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "grafbase-postgres"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
edition = "2024"
55
license = "Apache-2.0"
66

cli/postgres/README.md

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,103 @@ default_schema = "public"
106106
# maps to a Postgres database name in your gateway configuration.
107107
# Defaults to "default" if you don't specify it
108108
database_name = "default"
109+
110+
# Enable mutations (write operations) globally for the whole database.
111+
# Defaults to true if you omit this setting.
112+
enable_mutations = true
113+
114+
# Enable queries (read operations) globally for the whole database.
115+
# Defaults to true if you omit this setting.
116+
enable_queries = true
117+
118+
# Configure schemas for this database. Key-value from schema name to configuration.
119+
schemas = {}
120+
```
121+
122+
### Schema Configuration
123+
124+
```toml
125+
[schemas.public]
126+
127+
# Enable mutations (write operations) globally for the whole schema.
128+
# Takes precedence over the global setting. Defaults to true if you omit this setting.
129+
enable_mutations = true
130+
131+
# Enable queries (read operations) globally for the whole database.
132+
# Takes precedence over the global setting. Defaults to true if you omit this setting.
133+
enable_queries = true
134+
135+
# Configure views for this schema.
136+
views = {}
137+
138+
# Configure tables for this schema. Key-value from table name to configuration.
139+
tables = {}
109140
```
110141

111-
### Exposing Views
142+
### Table Configuration
143+
144+
```toml
145+
[schemas.public.tables.users]
146+
147+
# Enable mutations (write operations) for the table.
148+
# Takes precedence over the global and schema settings.
149+
# Defaults to true if you omit this setting.
150+
enable_mutations = true
151+
152+
# Enable queries (read operations) for the table.
153+
# Takes precedence over the global and schema settings.
154+
# Defaults to true if you omit this setting.
155+
enable_queries = true
156+
157+
# Table relations are always calculated from the database foreign keys.
158+
# In cases like with table to view relations this is not possible, and
159+
# you can define them manually from this map. Key/value from relation name
160+
# to config.
161+
relations = {}
162+
```
163+
164+
### View Configuration
112165

113166
PostgreSQL views require additional configuration because the information schema doesn't provide details about unique constraints, nullability, or relations. To make a view visible in your GraphQL SDL, you must define at least one unique key.
114167

168+
```toml
169+
[schemas.public.views.restricted_users]
170+
171+
# Enable queries (read operations) for the the view.
172+
# Takes precedence over the global and schema settings.
173+
# Defaults to true if you omit this setting.
174+
enable_queries = true
175+
176+
# Even if the underlying table has unique constraints, the database does not
177+
# show them for the view presenting the data. Single column keys you can configure
178+
# better through the columns map, but use this for compound keys.
179+
# An array of arrays. Each array is a collection of columns forming the key. Order
180+
# of the columns matter.
181+
unique_keys = []
182+
183+
# The database does not have information on the nullability, or uniqueness
184+
# of view columns. You can define column settings manually from this map.
185+
# Key/value from column name to config.
186+
columns = {}
187+
188+
# Views do not have foreign keys mapped to them, as tables do. You
189+
# can define relations manualy from this map.
190+
# Key/value from relation name to config.
191+
relations = {}
192+
```
193+
115194
#### Unique Key Definitions
116195

117196
```toml
118197
[schemas.public.views.my_view]
198+
119199
# The order of columns matters - match the order in the underlying query/table.
120200
# Define compound keys like this:
121201
unique_keys = [["user_name", "user_id"]]
122202

123203
# structure: schemas.<schema_name>.views.<view_name>.columns.<column_name>
124204
[schemas.public.views.my_view.columns.user_name]
205+
125206
# Defaults to true if you omit this setting
126207
nullable = false
127208
# Define a single-column unique key here. Defaults to false if omitted.
@@ -142,6 +223,7 @@ The introspection will fail if you reference any non-existent schemas, views, or
142223
```toml
143224
# structure: schemas.<schema_name>.views.<view_name>.relations.<relation_name>
144225
[schemas.public.views.my_view.relations.my_view_to_my_table]
226+
145227
# The schema containing the referenced table or view.
146228
# Defaults to "public" if omitted. Must exist.
147229
referenced_schema = "public"

crates/database-definition/src/table.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ impl RelationKind {
3030
RelationKind::MaterializedView => "MATERIALIZED_VIEW",
3131
}
3232
}
33+
34+
pub fn is_view(self) -> bool {
35+
!matches!(self, RelationKind::Relation)
36+
}
3337
}
3438

3539
impl<T> Copy for Table<T> where T: Copy {}

crates/postgres-introspection/src/config.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
1+
use grafbase_database_definition::TableWalker;
12
use serde::Deserialize;
23
use std::collections::HashMap;
34

5+
/// Returns the default value for enable_queries configuration.
6+
fn default_enable_queries() -> bool {
7+
true
8+
}
9+
10+
/// Returns the default value for enable_mutations configuration.
11+
fn default_enable_mutations() -> bool {
12+
true
13+
}
14+
415
/// Represents the overall configuration for the application.
516
#[derive(Deserialize, Debug)]
617
#[serde(deny_unknown_fields)]
718
pub struct Config {
19+
/// Determines whether mutations (write operations) are enabled for this configuration.
20+
#[serde(default = "default_enable_mutations")]
21+
pub enable_mutations: bool,
22+
/// Determines whether queries (read operations) are enabled for this configuration.
23+
#[serde(default = "default_enable_queries")]
24+
pub enable_queries: bool,
825
/// The name of the database this virtual subgraph uses (the name from grafbase.toml).
926
#[serde(default = "default_database_name")]
1027
pub database_name: String,
@@ -19,6 +36,46 @@ pub struct Config {
1936
pub schemas: HashMap<String, SchemaConfig>,
2037
}
2138

39+
impl Config {
40+
/// Determines whether mutations (write operations) are allowed for the specified table.
41+
pub fn mutations_allowed(&self, table: TableWalker<'_>) -> bool {
42+
if !table.mutations_allowed() {
43+
return false;
44+
}
45+
46+
let Some(schema_config) = self.schemas.get(table.schema()) else {
47+
return self.enable_mutations;
48+
};
49+
50+
let Some(table_config) = schema_config.tables.get(table.database_name()) else {
51+
return schema_config.enable_mutations;
52+
};
53+
54+
table_config.enable_mutations
55+
}
56+
57+
/// Determines whether queries (read operations) are allowed for the specified table.
58+
pub fn queries_allowed(&self, table: TableWalker<'_>) -> bool {
59+
let Some(schema_config) = self.schemas.get(table.schema()) else {
60+
return self.enable_queries;
61+
};
62+
63+
if table.relation_kind().is_view() {
64+
let Some(view_config) = schema_config.views.get(table.database_name()) else {
65+
return schema_config.enable_queries;
66+
};
67+
68+
view_config.enable_queries
69+
} else {
70+
let Some(table_config) = schema_config.tables.get(table.database_name()) else {
71+
return schema_config.enable_queries;
72+
};
73+
74+
table_config.enable_queries
75+
}
76+
}
77+
}
78+
2279
/// Returns the default database name.
2380
fn default_database_name() -> String {
2481
"default".to_string()
@@ -33,6 +90,12 @@ fn default_default_schema() -> String {
3390
#[derive(Deserialize, Debug)]
3491
#[serde(deny_unknown_fields)]
3592
pub struct SchemaConfig {
93+
/// Determines whether mutations (write operations) are enabled for this schema.
94+
#[serde(default = "default_enable_mutations")]
95+
pub enable_mutations: bool,
96+
/// Determines whether queries (read operations) are enabled for this schema.
97+
#[serde(default = "default_enable_queries")]
98+
pub enable_queries: bool,
3699
/// Configuration details for each view within the database, keyed by view name.
37100
#[serde(default)]
38101
pub views: HashMap<String, ViewConfig>,
@@ -41,10 +104,15 @@ pub struct SchemaConfig {
41104
pub tables: HashMap<String, TableConfig>,
42105
}
43106

44-
/// Represents the configuration settings for a specific database relation (e.g., a view).
45107
#[derive(Deserialize, Debug)]
46108
#[serde(deny_unknown_fields)]
47109
pub struct TableConfig {
110+
/// Determines whether mutations (write operations) are enabled for this table.
111+
#[serde(default = "default_enable_mutations")]
112+
pub enable_mutations: bool,
113+
/// Determines whether queries (read operations) are enabled for this table.
114+
#[serde(default = "default_enable_queries")]
115+
pub enable_queries: bool,
48116
/// Configuration details for relationships originating from this view, keyed by relationship name.
49117
#[serde(default)]
50118
pub relations: HashMap<String, RelationConfig>,
@@ -54,6 +122,12 @@ pub struct TableConfig {
54122
#[derive(Deserialize, Debug)]
55123
#[serde(deny_unknown_fields)]
56124
pub struct ViewConfig {
125+
/// Determines whether mutations (write operations) are enabled for this table.
126+
#[serde(default = "default_enable_mutations")]
127+
pub enable_mutations: bool,
128+
/// Determines whether queries (read operations) are enabled for this table.
129+
#[serde(default = "default_enable_queries")]
130+
pub enable_queries: bool,
57131
/// Optional list of unique key constraints, where each constraint is a list of column names.
58132
pub unique_keys: Option<Vec<Vec<String>>>,
59133
/// Configuration details for each column within the relation, keyed by column name.

crates/postgres-introspection/src/lib.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,7 @@ pub async fn introspect(conn: &mut sqlx::PgConnection, config: Config) -> anyhow
3232

3333
database_definition.finalize();
3434

35-
Ok(render::to_sdl(
36-
database_definition,
37-
&config.extension_url,
38-
&config.default_schema,
39-
&config.database_name,
40-
))
35+
Ok(render::to_sdl(database_definition, &config))
4136
}
4237

4338
/// A list of schemas to filter out automatically on every introspection.

crates/postgres-introspection/src/render.rs

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,29 @@ mod tables;
1111
use ast::schema::Schema;
1212
use grafbase_database_definition::DatabaseDefinition;
1313

14+
use crate::config::Config;
15+
1416
const DEFAULT_DATABASE_NAME: &str = "default";
1517

16-
pub fn to_sdl(
17-
database_definition: DatabaseDefinition,
18-
extension_url: &str,
19-
default_schema: &str,
20-
database_name: &str,
21-
) -> String {
18+
/// Defines if at least one table has queries or mutations enabled.
19+
#[derive(Debug, Clone, Copy)]
20+
struct EnabledOperations {
21+
has_queries: bool,
22+
has_mutations: bool,
23+
}
24+
25+
pub fn to_sdl(database_definition: DatabaseDefinition, config: &Config) -> String {
26+
let database_name = config.database_name.as_str();
27+
let extension_url = config.extension_url.as_str();
28+
let default_schema = config.default_schema.as_str();
29+
2230
let mut rendered = Schema::new();
2331

32+
let mut operations = EnabledOperations {
33+
has_queries: false,
34+
has_mutations: false,
35+
};
36+
2437
let prefix = if database_name == DEFAULT_DATABASE_NAME {
2538
None
2639
} else {
@@ -29,12 +42,18 @@ pub fn to_sdl(
2942

3043
scalars::render(&mut rendered);
3144
schema_directives::render(&database_definition, extension_url, &mut rendered);
32-
enums::render(&database_definition, default_schema, &mut rendered);
33-
input_types::render(&database_definition, prefix, &mut rendered);
34-
output_types::render(&database_definition, &mut rendered);
35-
tables::render(&database_definition, default_schema, &mut rendered);
36-
query::render(&database_definition, prefix, &mut rendered);
37-
mutation::render(&database_definition, prefix, &mut rendered);
45+
input_types::render(&database_definition, config, &mut operations, prefix, &mut rendered);
46+
enums::render(&database_definition, default_schema, &operations, &mut rendered);
47+
output_types::render(&database_definition, config, operations, &mut rendered);
48+
tables::render(&database_definition, default_schema, operations, &mut rendered);
49+
50+
if operations.has_queries {
51+
query::render(&database_definition, config, prefix, &mut rendered);
52+
}
53+
54+
if operations.has_mutations {
55+
mutation::render(&database_definition, config, prefix, &mut rendered);
56+
}
3857

3958
rendered.to_string()
4059
}

crates/postgres-introspection/src/render/enums.rs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
11
use grafbase_database_definition::DatabaseDefinition;
22

3-
use super::ast::{
4-
directive::{Argument, Directive},
5-
r#enum::{Enum, EnumVariant},
6-
schema::Schema,
3+
use super::{
4+
EnabledOperations,
5+
ast::{
6+
directive::{Argument, Directive},
7+
r#enum::{Enum, EnumVariant},
8+
schema::Schema,
9+
},
710
};
811

9-
pub fn render<'a>(database_definition: &'a DatabaseDefinition, default_schema: &'a str, rendered: &mut Schema<'a>) {
10-
rendered.push_enum({
11-
let mut r#enum = Enum::new("OrderDirection");
12-
r#enum.set_description("Specifies the direction for ordering results.");
12+
pub fn render<'a>(
13+
database_definition: &'a DatabaseDefinition,
14+
default_schema: &'a str,
15+
operations: &EnabledOperations,
16+
rendered: &mut Schema<'a>,
17+
) {
18+
if operations.has_queries {
19+
rendered.push_enum({
20+
let mut r#enum = Enum::new("OrderDirection");
21+
r#enum.set_description("Specifies the direction for ordering results.");
1322

14-
for (variant, description) in [
15-
("ASC", "Specifies an ascending order for a given orderBy argument."),
16-
("DESC", "Specifies a descending order for a given orderBy argument."),
17-
] {
18-
let mut variant = EnumVariant::new(variant);
19-
variant.set_description(description);
23+
for (variant, description) in [
24+
("ASC", "Specifies an ascending order for a given orderBy argument."),
25+
("DESC", "Specifies a descending order for a given orderBy argument."),
26+
] {
27+
let mut variant = EnumVariant::new(variant);
28+
variant.set_description(description);
2029

21-
r#enum.push_variant(variant);
22-
}
30+
r#enum.push_variant(variant);
31+
}
2332

24-
r#enum
25-
});
33+
r#enum
34+
});
35+
}
2636

2737
for r#enum in database_definition.enums() {
2838
let mut render = Enum::new(r#enum.client_name());

0 commit comments

Comments
 (0)