Skip to main content

postrust_graphql/
names.rs

1//! Names the schema does not carry.
2//!
3//! Almost everything in the generated API is derived: a table's own name gives
4//! the root fields, a foreign key gives a relationship, a function gives a
5//! computed field. Hasura derives none of it. Every one of those names is
6//! written down in metadata by a person -- `add_computed_field` says the
7//! function is `fetch_articles_plain` and the field is `get_articles`,
8//! `create_array_relationship` says the relationship is `posts` -- and
9//! reflection cannot recover a name nobody wrote down.
10//!
11//! That is the single largest remaining divergence from Hasura, and it does
12//! not shrink by implementing anything. Around 25 cases in Hasura's own corpus
13//! fail on it across four groups, every one of them by answering correctly
14//! under a different name.
15//!
16//! So names can be given -- and, since a permission is the same kind of thing,
17//! permissions too. Both are decisions a person wrote into metadata that no
18//! schema remembers. This is still a lookup table rather than a metadata model:
19//! it tracks no tables, offers no API to change it, and a table absent from it
20//! is exposed exactly as before.
21//!
22//! ```json
23//! {
24//!   "public.author": {
25//!     "name": "Authors",
26//!     "roots": { "select_by_pk": "Author" },
27//!     "columns": { "id": "AuthorId" },
28//!     "relationships": { "article_author_id_fkey": "posts" },
29//!     "computed_fields": { "automatic_comment_in_db_upper_name": "upper_name" },
30//!     "permissions": {
31//!       "user": {
32//!         "select": {
33//!           "columns": ["id", "name"],
34//!           "filter": { "id": "X-Hasura-User-Id" },
35//!           "limit": 10
36//!         }
37//!       }
38//!     }
39//!   }
40//! }
41//! ```
42//!
43//! The module is called `names` because names came first, and the document's
44//! variable keeps both spellings for the same reason. What it holds is now
45//! wider than that.
46//!
47//! Relationships and computed fields are keyed by the thing the database
48//! actually has -- a constraint name, or a function name -- rather than by the
49//! name being replaced. A derived name is what this exists to change, so
50//! keying by it would mean writing down the answer to ask the question; and
51//! where two foreign keys point at one table the derived names collide, which
52//! is one of the cases this is for.
53//!
54//! Columns are keyed by the column, which is the same rule: the column is what
55//! the database has. It is also the entry that reaches furthest -- a renamed
56//! column is a name in the schema and in nothing else, so every path from a
57//! request to SQL translates it back.
58
59use serde::{Deserialize, Serialize};
60use std::collections::HashMap;
61
62/// The names given to one table.
63#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
64#[non_exhaustive]
65pub struct TableNames {
66    /// The base name for this table's root fields and types.
67    ///
68    /// `"name": "Author"` gives `Author`, `Author_by_pk`, `insert_Author`,
69    /// `Author_bool_exp` and the rest, since all of them are derived from one
70    /// base name.
71    #[serde(default)]
72    pub name: Option<String>,
73
74    /// Relationship field names, keyed by constraint name -- or by function
75    /// name for a computed relationship, which has no constraint.
76    #[serde(default)]
77    pub relationships: HashMap<String, String>,
78
79    /// The relationships this table has, where metadata says which.
80    ///
81    /// `relationships` above renames what reflection found; this replaces it.
82    /// Two things need that, and they are the same thing seen from either end.
83    ///
84    /// A **manual** relationship maps column to column and has no constraint
85    /// to be keyed by, so `relationships` cannot carry it at all -- and a
86    /// permission written through one could not be compiled, which is a read
87    /// refused rather than narrowed.
88    ///
89    /// And **which** relationships exist is metadata's to say. Reflection
90    /// offers one per foreign key; Hasura offers the ones its metadata
91    /// declares, which is usually fewer. A table whose keys are all declared
92    /// looks the same either way, and one that tracks only some of them does
93    /// not.
94    ///
95    /// Absent means "reflect", which is what a document that says nothing
96    /// about a table has always meant. Present means "these, and no others".
97    #[serde(default)]
98    pub declared_relationships: Option<Vec<DeclaredRelationship>>,
99
100    /// Computed field names, keyed by the function behind them.
101    #[serde(default)]
102    pub computed_fields: HashMap<String, String>,
103
104    /// Root field names, keyed by the root they replace: `select`,
105    /// `select_by_pk`, `select_aggregate`, `insert`, `insert_one`, `update`,
106    /// `update_by_pk`, `delete`, `delete_by_pk`.
107    ///
108    /// Separate from `name` because Hasura names each root independently --
109    /// `select: Authors`, `select_by_pk: Author`, `select_aggregate:
110    /// AuthorAgg` -- where a base name derives all of them from one word. A
111    /// set that agrees on a base could be written as `name`; one that does not
112    /// can only be written here.
113    #[serde(default)]
114    pub roots: HashMap<String, String>,
115
116    /// Descriptions Hasura keeps in metadata rather than in the database.
117    ///
118    /// A comment is not a name, and is here for the same reason the names are:
119    /// where metadata carries one, the database's own comment is not what a
120    /// client sees. An empty string is "no description", which is how
121    /// `set_table_customization` suppresses a comment the database has.
122    #[serde(default)]
123    pub comments: Comments,
124
125    /// Field names for columns, keyed by the column.
126    ///
127    /// The one rename that is not a name for something derived: a column has a
128    /// name, and this is a different one to expose it under. It reaches
129    /// further than the others because a column appears in the projection, in
130    /// `where`, in `order_by`, in `distinct_on`, in both mutation inputs and
131    /// in every embed -- everywhere a field name has to become SQL.
132    #[serde(default)]
133    pub columns: HashMap<String, String>,
134
135    /// GraphQL scalar overrides for columns, keyed by the database column.
136    ///
137    /// PostgreSQL has no `ID` type: identifiers are commonly stored as text,
138    /// integers or UUIDs. This lets the public GraphQL contract mark a column
139    /// as an opaque identifier without changing the type used to read, bind or
140    /// cast that column in PostgreSQL.
141    #[serde(default)]
142    pub column_types: HashMap<String, ColumnTypeOverride>,
143
144    /// Whether this table is a set of allowed values rather than a set of
145    /// rows.
146    ///
147    /// The one thing here that is not a name, and it is here for the same
148    /// reason the names are: nothing in the schema says it. A table with a
149    /// text primary key and a comment column is an ordinary table -- being an
150    /// enumeration is a decision someone made about it, which Hasura records
151    /// as `set_table_is_enum` and which reflection cannot recover.
152    ///
153    /// Marked so, its rows become the members of a GraphQL enum and every
154    /// column with a foreign key to it is typed as that enum instead of as
155    /// text.
156    #[serde(default, rename = "enum")]
157    pub is_enum: bool,
158
159    /// Apollo Federation settings for this table.
160    #[serde(default)]
161    pub federation: FederationConfig,
162
163    /// What each role may do with this table, keyed by role.
164    ///
165    /// A role absent from this map has no permission on the table at all,
166    /// which in Hasura is not a refusal at execution but an absence in the
167    /// schema: the root fields are not there to be named. The same is true one
168    /// level down -- a role with `select` and no `insert` has no `insert_x`
169    /// field, and asking for one is a validation failure rather than a denial.
170    ///
171    /// Empty for every table until a document says otherwise, and a document
172    /// that says nothing about permissions leaves the server behaving as it did
173    /// before they existed: database roles and row level security, with no
174    /// second layer above them.
175    #[serde(default)]
176    pub permissions: HashMap<String, RolePermissions>,
177}
178
179/// Apollo Federation settings for one table.
180#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
181#[non_exhaustive]
182pub struct FederationConfig {
183    /// Whether this table keeps its unprefixed entity identity across subgraphs.
184    #[serde(default)]
185    pub shared: bool,
186}
187
188/// A GraphQL scalar a database column may be exposed as.
189///
190/// Only `ID` is supported for now. Unlike the other built-in scalars, `ID`
191/// has no PostgreSQL type from which it can be inferred, and strings returned
192/// by PostgreSQL are already valid `ID` values.
193#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
194#[non_exhaustive]
195pub enum ColumnTypeOverride {
196    /// GraphQL's opaque identifier scalar.
197    #[serde(rename = "ID")]
198    Id,
199}
200
201impl ColumnTypeOverride {
202    /// The schema type represented by this override.
203    pub fn graphql_type(self) -> crate::types::GraphQLType {
204        match self {
205            Self::Id => crate::types::GraphQLType::Id,
206        }
207    }
208}
209
210impl RolePermissions {
211    /// The columns one verb may name, where that verb names columns at all.
212    pub fn write_columns(&self, verb: crate::role::Verb) -> Option<&ColumnSet> {
213        match verb {
214            crate::role::Verb::Insert => self.insert.as_ref().map(|p| &p.columns),
215            crate::role::Verb::Update => self.update.as_ref().map(|p| &p.columns),
216            crate::role::Verb::Select | crate::role::Verb::Delete => None,
217        }
218    }
219}
220
221/// What one role may do with one table.
222///
223/// Four independent grants, and the absence of one is meaningful: no `select`
224/// means the table cannot be read by this role, not that it can be read
225/// without restriction.
226#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
227#[non_exhaustive]
228pub struct RolePermissions {
229    /// Which rows the role may read. Absent means it may read none.
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub select: Option<SelectPermission>,
232    /// What the role may insert. Absent means it may insert nothing.
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub insert: Option<InsertPermission>,
235    /// Which rows the role may change. Absent means it may change none.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub update: Option<UpdatePermission>,
238    /// Which rows the role may delete. Absent means it may delete none.
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub delete: Option<DeletePermission>,
241}
242
243/// Which rows a role may read, and which of their columns.
244#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
245#[non_exhaustive]
246pub struct SelectPermission {
247    /// The columns this role can see. Every other column of the table is not
248    /// merely unreadable but absent from the type, which is what makes a
249    /// permission a statement about the schema rather than about a request.
250    #[serde(default)]
251    pub columns: ColumnSet,
252
253    /// Which rows. A boolean expression in the same shape a `where` argument
254    /// takes, with one addition: a string like `X-Hasura-User-Id` stands for
255    /// the caller's session variable of that name.
256    ///
257    /// Left uninterpreted here. Reading it is the query builder's job, and a
258    /// document that carries a predicate this server cannot compile should
259    /// fail where the compilation is, not where the parsing is.
260    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
261    pub filter: serde_json::Value,
262
263    /// The most rows a single request may read. A ceiling, not a default: a
264    /// request asking for more gets this, and one asking for fewer gets what it
265    /// asked for.
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub limit: Option<u64>,
268
269    /// Whether this role may ask for aggregates. Hasura keeps it separate from
270    /// reading rows because counting rows you cannot see is a way of seeing
271    /// them.
272    #[serde(default)]
273    pub allow_aggregations: bool,
274
275    /// Which computed fields this role may ask for. Empty is none, not all:
276    /// a computed field runs a function over a row, so it can answer questions
277    /// the column permissions were written to prevent.
278    #[serde(default, skip_serializing_if = "Vec::is_empty")]
279    pub computed_fields: Vec<String>,
280}
281
282/// What a role may write, and what the result has to satisfy.
283#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
284#[non_exhaustive]
285pub struct InsertPermission {
286    /// The columns a request may supply.
287    #[serde(default)]
288    pub columns: ColumnSet,
289
290    /// What every written row must satisfy. Unlike a `filter`, this is checked
291    /// against the row *after* it is built, which is what stops a caller
292    /// inserting a row it would not then be allowed to read.
293    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
294    pub check: serde_json::Value,
295
296    /// Columns the server fills in, overriding whatever the request said. The
297    /// value may be a session variable, which is how `author_id` comes from
298    /// the caller's identity rather than from the caller.
299    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
300    pub set: HashMap<String, serde_json::Value>,
301
302    /// Reachable only by a caller that proved it holds the admin secret,
303    /// whatever role it then claims. A mutation a client must not be able to
304    /// reach by naming a role.
305    #[serde(default)]
306    pub backend_only: bool,
307}
308
309/// Which rows a role may change, which columns of them, and what the result
310/// has to satisfy.
311#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
312#[non_exhaustive]
313pub struct UpdatePermission {
314    /// The columns the role may write.
315    #[serde(default)]
316    pub columns: ColumnSet,
317
318    /// Which rows may be changed, read before the change.
319    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
320    pub filter: serde_json::Value,
321
322    /// What they must satisfy afterwards. Absent means the `filter` stands in,
323    /// which is Hasura's rule: a row you may change is a row you may change
324    /// into something you may still change.
325    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
326    pub check: serde_json::Value,
327
328    /// Columns forced to a fixed value on every update, whatever the request
329    /// asked for. Hasura's `set`, used for things like an updated-at stamp.
330    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
331    pub set: HashMap<String, serde_json::Value>,
332}
333
334/// Which rows a role may delete.
335#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
336#[non_exhaustive]
337pub struct DeletePermission {
338    /// Which rows may be deleted. Null means every row the role can see.
339    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
340    pub filter: serde_json::Value,
341}
342
343/// The columns a permission covers.
344///
345/// Hasura writes either a list or `"*"`, and the two are not the same thing
346/// even when they name the same columns today: `"*"` follows the table, so a
347/// column added tomorrow is covered, and a list does not.
348#[derive(Debug, Clone, Default, PartialEq)]
349pub enum ColumnSet {
350    /// Every column the table has, now and later.
351    #[default]
352    All,
353    /// Exactly these.
354    Named(Vec<String>),
355}
356
357impl ColumnSet {
358    /// Whether a column is covered.
359    pub fn allows(&self, column: &str) -> bool {
360        match self {
361            Self::All => true,
362            Self::Named(columns) => columns.iter().any(|name| name == column),
363        }
364    }
365
366    /// Whether this covers nothing at all.
367    ///
368    /// A permission naming no columns is one Hasura accepts and which grants
369    /// only the ability to know the table exists.
370    pub fn is_empty(&self) -> bool {
371        matches!(self, Self::Named(columns) if columns.is_empty())
372    }
373}
374
375impl Serialize for ColumnSet {
376    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
377        match self {
378            Self::All => serializer.serialize_str("*"),
379            Self::Named(columns) => columns.serialize(serializer),
380        }
381    }
382}
383
384impl<'de> Deserialize<'de> for ColumnSet {
385    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
386        #[derive(Deserialize)]
387        #[serde(untagged)]
388        enum Either {
389            Wildcard(String),
390            Named(Vec<String>),
391        }
392
393        match Either::deserialize(deserializer)? {
394            Either::Named(columns) => Ok(Self::Named(columns)),
395            Either::Wildcard(text) if text == "*" => Ok(Self::All),
396            Either::Wildcard(text) => Err(serde::de::Error::custom(format!(
397                "\"{}\" is not a column set; write a list of columns, or \"*\" for all of them",
398                text
399            ))),
400        }
401    }
402}
403
404/// Descriptions given to one table and the things on it.
405///
406/// Every value is a description as written: an empty string means the field
407/// has none, which is a different answer from having said nothing.
408#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
409#[non_exhaustive]
410pub struct Comments {
411    /// The description of the table's own type.
412    #[serde(default)]
413    pub table: Option<String>,
414    /// Column descriptions, keyed by the column.
415    #[serde(default)]
416    pub columns: HashMap<String, String>,
417    /// Computed field descriptions, keyed by the function behind them.
418    #[serde(default)]
419    pub computed_fields: HashMap<String, String>,
420    /// Root field descriptions, keyed by the root: `select`, `insert_one`.
421    #[serde(default)]
422    pub roots: HashMap<String, String>,
423}
424
425/// Every name given, keyed by `schema.table`.
426#[derive(Debug, Clone, Default, PartialEq)]
427#[non_exhaustive]
428pub struct NameOverrides {
429    tables: HashMap<String, TableNames>,
430    functions: HashMap<String, FunctionNames>,
431    introspection_disabled_for: Vec<String>,
432}
433
434/// The document in its sectioned shape.
435#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
436struct Sections {
437    #[serde(default)]
438    tables: HashMap<String, TableNames>,
439    #[serde(default)]
440    functions: HashMap<String, FunctionNames>,
441    /// Roles that may not read the schema as data.
442    ///
443    /// Not per table, unlike everything else here, because the question is not
444    /// about a table: Hasura's `set_graphql_schema_introspection_options` names
445    /// roles and nothing else.
446    #[serde(default)]
447    introspection_disabled_for: Vec<String>,
448}
449
450/// One relationship metadata declares, and how it is reached.
451///
452/// Either by a key reflection already found -- `using` names the constraint or
453/// the column, the same spellings [`TableNames::relationships`] accepts -- or
454/// by a column mapping, which is a join no key describes.
455#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
456#[non_exhaustive]
457pub struct DeclaredRelationship {
458    /// The field name this relationship is exposed under.
459    pub name: String,
460    /// The key it is reached by, where it is reached by one.
461    #[serde(default)]
462    pub using: Option<String>,
463    /// The table it points at, as `schema.table`, for a mapped join.
464    #[serde(default)]
465    pub table: Option<String>,
466    /// The join, as this table's column to the other table's column.
467    #[serde(default)]
468    pub columns: std::collections::BTreeMap<String, String>,
469    /// Whether it yields one row rather than many.
470    ///
471    /// A key says this for itself; a column mapping does not, which is why
472    /// Hasura declares object and array relationships separately.
473    #[serde(default)]
474    pub to_one: bool,
475    /// Whether a nested insert writes this row *after* the row it hangs off.
476    ///
477    /// A key says which side holds it: `article.author_id` references
478    /// `author`, so the author is written first and its key fills the
479    /// article's column. A column mapping says only that two columns are
480    /// equal, and `author.id = author_detail.id` is equal in both directions
481    /// -- so which row can be written first is not recoverable from it, and
482    /// Hasura writes `insertion_order` down for exactly that reason.
483    ///
484    /// `false` is `before_parent`, which is what an object relationship means
485    /// without being told otherwise. Ignored for a relationship to many,
486    /// which can only be written after the row it hangs off.
487    #[serde(default)]
488    pub after_parent: bool,
489}
490
491impl DeclaredRelationship {
492    /// The table a mapped join points at, as `(schema, table)`.
493    ///
494    /// `None` for one reached by a key, which needs no target written down:
495    /// the key already says where it goes.
496    pub fn target(&self, default_schema: &str) -> Option<(String, String)> {
497        let written = self.table.as_deref()?;
498        Some(match written.split_once('.') {
499            Some((schema, table)) => (schema.to_string(), table.to_string()),
500            None => (default_schema.to_string(), written.to_string()),
501        })
502    }
503}
504
505/// What metadata says about one function, beyond what the catalogue knows.
506#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
507#[non_exhaustive]
508pub struct FunctionNames {
509    /// The root this function is exposed on: `query` or `mutation`.
510    #[serde(default)]
511    pub exposed_as: Option<String>,
512    /// The roles a function permission names.
513    ///
514    /// Hasura infers a *query* function's permission from the select
515    /// permission on the table it returns -- a role that may read the rows may
516    /// ask the function for them. A function exposed as a **mutation** is not
517    /// inferred from anything: it has side effects, and a permission to read a
518    /// table is not a permission to change it, so the role has to be named by
519    /// a `pg_create_function_permission`.
520    #[serde(default)]
521    pub roles: Vec<String>,
522}
523
524impl NameOverrides {
525    /// Read from JSON: either the document itself, or a path to a file holding
526    /// it.
527    ///
528    /// Told apart by the first non-space character, because a JSON object
529    /// cannot begin with anything but `{` and a path cannot begin with it.
530    /// Both spellings exist because both places are natural: a handful of
531    /// names belongs in the environment beside every other setting, and a
532    /// migrated schema's worth of them belongs in a file under review.
533    pub fn parse(value: &str) -> Result<Self, String> {
534        let trimmed = value.trim();
535        if trimmed.is_empty() {
536            return Ok(Self::default());
537        }
538
539        let document = if trimmed.starts_with('{') {
540            trimmed.to_string()
541        } else {
542            std::fs::read_to_string(trimmed)
543                .map_err(|e| format!("cannot read GraphQL names from \"{}\": {}", trimmed, e))?
544        };
545
546        // Two shapes, and which one this is can be told from the keys: a
547        // table key is `schema.table` and always has a dot, so `tables` and
548        // `functions` at the top level cannot be tables and can only be the
549        // sections they name. The flat shape came first and stays valid.
550        let parsed: serde_json::Value = serde_json::from_str(&document)
551            .map_err(|e| format!("GraphQL names are not valid JSON: {}", e))?;
552        let sectioned = parsed.as_object().is_some_and(|map| {
553            map.contains_key("tables")
554                || map.contains_key("functions")
555                || map.contains_key("introspection_disabled_for")
556        });
557
558        let (tables, functions, introspection_disabled_for) = match sectioned {
559            true => {
560                let document: Sections = serde_json::from_value(parsed)
561                    .map_err(|e| format!("GraphQL names are not valid JSON: {}", e))?;
562                (
563                    document.tables,
564                    document.functions,
565                    document.introspection_disabled_for,
566                )
567            }
568            false => (
569                serde_json::from_value(parsed)
570                    .map_err(|e| format!("GraphQL names are not valid JSON: {}", e))?,
571                HashMap::new(),
572                Vec::new(),
573            ),
574        };
575
576        for key in functions.keys() {
577            if !key.contains('.') {
578                return Err(format!(
579                    "\"{}\" does not name a function; keys are \"schema.function\", \
580                     so a function in the default schema is still \"public.{}\"",
581                    key, key
582                ));
583            }
584        }
585
586        for (key, names) in &tables {
587            if !key.contains('.') {
588                return Err(format!(
589                    "\"{}\" does not name a table; keys are \"schema.table\", \
590                     so a table in the default schema is still \"public.{}\"",
591                    key, key
592                ));
593            }
594            if names.name.as_deref() == Some("") {
595                return Err(format!("\"{}\" is given an empty name", key));
596            }
597        }
598
599        Ok(Self {
600            tables,
601            functions,
602            introspection_disabled_for,
603        })
604    }
605
606    /// Whether any name was given at all.
607    pub fn is_empty(&self) -> bool {
608        self.tables.is_empty() && self.functions.is_empty()
609    }
610
611    /// Which root a function was placed on, if metadata said.
612    ///
613    /// `query` or `mutation`. Reflection places a function by its volatility,
614    /// which is the only thing the catalogue records; Hasura lets metadata
615    /// override it -- `track_function` with `configuration: {exposed_as:
616    /// query}` puts a VOLATILE function on the query root, which is a thing a
617    /// person decided and no schema remembers.
618    pub fn exposed_as(&self, schema: &str, function: &str) -> Option<&str> {
619        self.functions
620            .get(&format!("{}.{}", schema, function))?
621            .exposed_as
622            .as_deref()
623    }
624
625    /// Whether a role was granted this function by a function permission.
626    ///
627    /// Only asked of a function exposed as a mutation -- see [`FunctionNames`]
628    /// for why the query side is inferred instead.
629    pub fn function_grants(&self, schema: &str, function: &str, role: &str) -> bool {
630        self.functions
631            .get(&format!("{}.{}", schema, function))
632            .is_some_and(|given| given.roles.iter().any(|named| named == role))
633    }
634
635    /// The relationships metadata declares for a table, where it declares any.
636    pub fn declared_relationships(
637        &self,
638        schema: &str,
639        table: &str,
640    ) -> Option<&[DeclaredRelationship]> {
641        self.table(schema, table)?.declared_relationships.as_deref()
642    }
643
644    /// How many tables were named, for the line the server logs at startup.
645    pub fn len(&self) -> usize {
646        self.tables.len()
647    }
648
649    /// How many functions were placed, for the same line.
650    pub fn placed_functions(&self) -> usize {
651        self.functions.len()
652    }
653
654    fn table(&self, schema: &str, table: &str) -> Option<&TableNames> {
655        self.tables.get(&format!("{}.{}", schema, table))
656    }
657
658    /// What one role may do with one table.
659    ///
660    /// `None` means the role was named nothing about this table, which is not
661    /// the same as being named an empty permission: the first has no access,
662    /// the second has access to nothing. Both refuse, and only the second is a
663    /// decision someone wrote down.
664    pub fn permissions(&self, schema: &str, table: &str, role: &str) -> Option<&RolePermissions> {
665        self.table(schema, table)?.permissions.get(role)
666    }
667
668    /// What one role was granted, across every table that grants it anything.
669    pub fn tables_with_permissions<'a>(
670        &'a self,
671        role: &'a str,
672    ) -> impl Iterator<Item = &'a RolePermissions> + 'a {
673        self.tables
674            .values()
675            .filter_map(move |table| table.permissions.get(role))
676    }
677
678    /// Whether this role may read the schema as data.
679    ///
680    /// `__typename` is not this question: it names the type of a row a client
681    /// is already reading, every client library sends it, and Hasura answers it
682    /// for a role introspection is disabled for. `__schema` and `__type` are
683    /// the two that read the schema itself.
684    pub fn introspection_disabled(&self, role: &str) -> bool {
685        self.introspection_disabled_for.iter().any(|it| it == role)
686    }
687
688    /// Whether the document says anything about permissions at all.
689    ///
690    /// The switch for the whole layer. A document carrying only names leaves
691    /// the server as it was: database roles and row level security, with
692    /// nothing above them. One permission anywhere turns the layer on for
693    /// every table, because a document that grants `user` a filtered view of
694    /// `article` and says nothing about `author` is saying `user` cannot read
695    /// `author` -- not that `author` is open to everyone.
696    pub fn has_permissions(&self) -> bool {
697        self.tables
698            .values()
699            .any(|table| !table.permissions.is_empty())
700    }
701
702    /// Every role the document names, sorted.
703    ///
704    /// Sorted because a schema is built per role and the order they are built
705    /// in should not depend on how a hash map felt. `admin` is not among them
706    /// unless a permission names it: an administrator is not a role with
707    /// permissions but a caller the permissions do not apply to.
708    pub fn roles(&self) -> Vec<&str> {
709        let mut roles: Vec<&str> = self
710            .tables
711            .values()
712            .flat_map(|table| table.permissions.keys())
713            .map(String::as_str)
714            .collect();
715        roles.sort_unstable();
716        roles.dedup();
717        roles
718    }
719
720    /// How many table-and-role pairs were granted something, for the line the
721    /// server logs at startup.
722    pub fn granted(&self) -> usize {
723        self.tables
724            .values()
725            .map(|table| table.permissions.len())
726            .sum()
727    }
728
729    /// Whether a table was marked as a set of allowed values.
730    pub fn is_enum(&self, schema: &str, table: &str) -> bool {
731        self.table(schema, table)
732            .map(|t| t.is_enum)
733            .unwrap_or(false)
734    }
735
736    /// Whether metadata marks this table as a shared Apollo Federation entity.
737    pub fn federation_shared(&self, schema: &str, table: &str) -> bool {
738        self.table(schema, table)
739            .map(|t| t.federation.shared)
740            .unwrap_or(false)
741    }
742
743    /// Every table marked as one, as `(schema, table)`.
744    pub fn enum_tables(&self) -> Vec<(String, String)> {
745        self.tables
746            .iter()
747            .filter(|(_, names)| names.is_enum)
748            .filter_map(|(key, _)| {
749                key.split_once('.')
750                    .map(|(schema, table)| (schema.to_string(), table.to_string()))
751            })
752            .collect()
753    }
754
755    /// The base name given to a table, if one was.
756    pub fn base_name(&self, schema: &str, table: &str) -> Option<&str> {
757        self.table(schema, table)?.name.as_deref()
758    }
759
760    /// The name given to a relationship, by its constraint, column or
761    /// function.
762    ///
763    /// Falls back to `computed_fields` because Hasura has one command for
764    /// both: `add_computed_field` covers a function returning a value and a
765    /// function returning rows, and which it is cannot be told from the
766    /// metadata -- only from the database. A converter should not have to
767    /// connect to one to put an entry in the right map, so either map answers.
768    pub fn relationship(&self, schema: &str, table: &str, source: &str) -> Option<&str> {
769        let names = self.table(schema, table)?;
770        names
771            .relationships
772            .get(source)
773            .or_else(|| names.computed_fields.get(source))
774            .map(String::as_str)
775    }
776
777    /// The name given to one root field, if one was.
778    ///
779    /// `kind` is Hasura's own key -- `select`, `insert_one`, `delete_by_pk`.
780    pub fn root(&self, schema: &str, table: &str, kind: &str) -> Option<&str> {
781        self.table(schema, table)?
782            .roots
783            .get(kind)
784            .map(String::as_str)
785    }
786
787    /// The field a column is exposed as, if it was renamed.
788    pub fn column(&self, schema: &str, table: &str, column: &str) -> Option<&str> {
789        self.table(schema, table)?
790            .columns
791            .get(column)
792            .map(String::as_str)
793    }
794
795    /// The GraphQL scalar a column is exposed as, if metadata overrides it.
796    pub fn column_type(
797        &self,
798        schema: &str,
799        table: &str,
800        column: &str,
801    ) -> Option<crate::types::GraphQLType> {
802        self.table(schema, table)?
803            .column_types
804            .get(column)
805            .copied()
806            .map(ColumnTypeOverride::graphql_type)
807    }
808
809    /// The column behind a field name, if that name is a rename.
810    ///
811    /// The other direction, which is the one every resolver needs: it is
812    /// handed a field name from the request and has to write a column. `None`
813    /// means the name is already the column's, which is the ordinary case.
814    pub fn column_source(&self, schema: &str, table: &str, field: &str) -> Option<&str> {
815        let names = self.table(schema, table)?;
816        names
817            .columns
818            .iter()
819            .find(|(_, exposed)| exposed.as_str() == field)
820            .map(|(column, _)| column.as_str())
821    }
822
823    /// Whether any column of this table is exposed under another name.
824    ///
825    /// Worth asking before building a projection: a table with no renames
826    /// keeps the `SELECT *` it had, and only one that has them pays for a
827    /// column list.
828    pub fn renames_columns(&self, schema: &str, table: &str) -> bool {
829        self.table(schema, table)
830            .map(|names| !names.columns.is_empty())
831            .unwrap_or(false)
832    }
833
834    /// The description given to a table's type, if one was.
835    ///
836    /// `Some("")` is a description that was given and is empty, which means
837    /// the type has none. `None` means nothing was said and the database's own
838    /// comment stands.
839    pub fn table_comment(&self, schema: &str, table: &str) -> Option<&str> {
840        self.table(schema, table)?.comments.table.as_deref()
841    }
842
843    /// The description given to a column, by the column.
844    pub fn column_comment(&self, schema: &str, table: &str, column: &str) -> Option<&str> {
845        self.table(schema, table)?
846            .comments
847            .columns
848            .get(column)
849            .map(String::as_str)
850    }
851
852    /// The description given to a computed field, by its function.
853    pub fn computed_comment(&self, schema: &str, table: &str, function: &str) -> Option<&str> {
854        self.table(schema, table)?
855            .comments
856            .computed_fields
857            .get(function)
858            .map(String::as_str)
859    }
860
861    /// The description given to one root field, by its kind.
862    pub fn root_comment(&self, schema: &str, table: &str, kind: &str) -> Option<&str> {
863        self.table(schema, table)?
864            .comments
865            .roots
866            .get(kind)
867            .map(String::as_str)
868    }
869
870    /// The name given to a computed field, by its function.
871    ///
872    /// Reads both maps, for the reason [`Self::relationship`] gives.
873    pub fn computed_field(&self, schema: &str, table: &str, function: &str) -> Option<&str> {
874        let names = self.table(schema, table)?;
875        names
876            .computed_fields
877            .get(function)
878            .or_else(|| names.relationships.get(function))
879            .map(String::as_str)
880    }
881
882    /// The function behind a computed field, given the name it is exposed
883    /// under.
884    ///
885    /// The resolver reads the other direction: it is handed a field name from
886    /// the selection and has to write the call. Searched rather than indexed
887    /// because a table has a handful of computed fields, not thousands, and a
888    /// second map would be one more thing to keep in step.
889    pub fn computed_source(&self, schema: &str, table: &str, field: &str) -> Option<&str> {
890        let names = self.table(schema, table)?;
891        names
892            .computed_fields
893            .iter()
894            .chain(names.relationships.iter())
895            .find(|(_, exposed)| exposed.as_str() == field)
896            .map(|(function, _)| function.as_str())
897    }
898}
899
900#[cfg(test)]
901mod tests {
902    use super::*;
903    use pretty_assertions::assert_eq;
904
905    const SAMPLE: &str = r#"{
906        "public.author": {
907            "name": "Author",
908            "relationships": {"article_author_id_fkey": "posts",
909                              "fetch_articles_plain": "get_articles"},
910            "computed_fields": {"author_upper_name": "upper_name"}
911        }
912    }"#;
913
914    #[test]
915    fn names_are_read_from_a_document() {
916        let names = NameOverrides::parse(SAMPLE).unwrap();
917        assert_eq!(names.base_name("public", "author"), Some("Author"));
918        assert_eq!(
919            names.relationship("public", "author", "article_author_id_fkey"),
920            Some("posts")
921        );
922        assert_eq!(
923            names.computed_field("public", "author", "author_upper_name"),
924            Some("upper_name")
925        );
926    }
927
928    #[test]
929    fn a_computed_field_can_be_looked_up_from_either_side() {
930        let names = NameOverrides::parse(SAMPLE).unwrap();
931        assert_eq!(
932            names.computed_source("public", "author", "upper_name"),
933            Some("author_upper_name")
934        );
935        assert_eq!(names.computed_source("public", "author", "nothing"), None);
936    }
937
938    #[test]
939    fn a_computed_field_is_found_in_either_map() {
940        // Hasura has one command for both, and which one an entry belongs in
941        // cannot be told from its metadata.
942        let names = NameOverrides::parse(
943            r#"{"public.author": {"computed_fields": {"fetch_articles_plain": "get_articles"}}}"#,
944        )
945        .unwrap();
946        assert_eq!(
947            names.relationship("public", "author", "fetch_articles_plain"),
948            Some("get_articles")
949        );
950
951        let other = NameOverrides::parse(
952            r#"{"public.author": {"relationships": {"author_upper_name": "upper_name"}}}"#,
953        )
954        .unwrap();
955        assert_eq!(
956            other.computed_field("public", "author", "author_upper_name"),
957            Some("upper_name")
958        );
959        assert_eq!(
960            other.computed_source("public", "author", "upper_name"),
961            Some("author_upper_name")
962        );
963    }
964
965    #[test]
966    fn a_table_can_be_marked_as_a_set_of_values() {
967        let names = NameOverrides::parse(
968            r#"{"public.colors": {"enum": true}, "public.author": {"name": "Author"}}"#,
969        )
970        .unwrap();
971        assert!(names.is_enum("public", "colors"));
972        assert!(!names.is_enum("public", "author"));
973        assert_eq!(
974            names.enum_tables(),
975            vec![("public".to_string(), "colors".to_string())]
976        );
977    }
978
979    #[test]
980    fn a_table_can_be_marked_as_a_shared_federation_entity() {
981        let names = NameOverrides::parse(
982            r#"{"tables": {"public.users": {"federation": {"shared": true}}}}"#,
983        )
984        .unwrap();
985
986        assert!(names.federation_shared("public", "users"));
987        assert!(!names.federation_shared("public", "posts"));
988    }
989
990    #[test]
991    fn a_column_can_be_exposed_as_an_id() {
992        let names = NameOverrides::parse(
993            r#"{"tables": {"public.users": {"column_types": {"email": "ID"}}}}"#,
994        )
995        .unwrap();
996
997        assert_eq!(
998            names.column_type("public", "users", "email"),
999            Some(crate::types::GraphQLType::Id)
1000        );
1001        assert_eq!(names.column_type("public", "users", "name"), None);
1002    }
1003
1004    #[test]
1005    fn an_unknown_column_type_override_is_rejected() {
1006        let error = NameOverrides::parse(
1007            r#"{"tables": {"public.users": {"column_types": {"email": "String"}}}}"#,
1008        )
1009        .expect_err("only supported overrides parse");
1010
1011        assert!(error.contains("unknown variant `String`"), "{error}");
1012    }
1013
1014    /// A declaration is the whole list, and it may describe a join.
1015    #[test]
1016    fn declared_relationships_carry_a_join_and_not_just_a_name() {
1017        let names = NameOverrides::parse(
1018            r#"{"tables": {"public.message": {"declared_relationships": [
1019                 {"name": "members", "table": "public.channel_member",
1020                  "columns": {"channel_id": "channel_id"}},
1021                 {"name": "author", "using": "message_author_id_fkey", "to_one": true}
1022               ]}}}"#,
1023        )
1024        .unwrap();
1025        let declared = names
1026            .declared_relationships("public", "message")
1027            .expect("the table declares some");
1028        assert_eq!(declared.len(), 2);
1029        assert_eq!(declared[0].name, "members");
1030        assert_eq!(
1031            declared[0].target("public"),
1032            Some(("public".to_string(), "channel_member".to_string()))
1033        );
1034        assert_eq!(
1035            declared[0].columns.get("channel_id").map(String::as_str),
1036            Some("channel_id")
1037        );
1038        assert!(!declared[0].to_one, "an array relationship by default");
1039        assert_eq!(declared[1].using.as_deref(), Some("message_author_id_fkey"));
1040        assert!(declared[1].to_one);
1041        // An unqualified table is read in the table's own schema.
1042        assert_eq!(
1043            crate::names::DeclaredRelationship {
1044                table: Some("channel_member".to_string()),
1045                ..Default::default()
1046            }
1047            .target("other"),
1048            Some(("other".to_string(), "channel_member".to_string()))
1049        );
1050        // And a table that declares nothing is left to reflection.
1051        assert_eq!(names.declared_relationships("public", "author"), None);
1052    }
1053
1054    #[test]
1055    fn a_table_that_was_not_named_is_left_alone() {
1056        let names = NameOverrides::parse(SAMPLE).unwrap();
1057        assert_eq!(names.base_name("public", "article"), None);
1058        assert_eq!(names.relationship("other", "author", "anything"), None);
1059    }
1060
1061    /// The sectioned shape, told apart from the flat one by a key that
1062    /// cannot be a table: a table key always carries a dot.
1063    #[test]
1064    fn a_function_can_be_placed_on_a_root() {
1065        let names = NameOverrides::parse(
1066            r#"{"tables": {"public.author": {"name": "Authors"}},
1067                "functions": {"public.volatile_func1": {"exposed_as": "query"}}}"#,
1068        )
1069        .unwrap();
1070        assert_eq!(names.base_name("public", "author"), Some("Authors"));
1071        assert_eq!(names.exposed_as("public", "volatile_func1"), Some("query"));
1072        assert_eq!(names.exposed_as("public", "something_else"), None);
1073    }
1074
1075    #[test]
1076    fn the_flat_shape_is_still_read() {
1077        let names = NameOverrides::parse(SAMPLE).unwrap();
1078        assert!(!names.is_empty());
1079        assert_eq!(names.exposed_as("public", "anything"), None);
1080    }
1081
1082    #[test]
1083    fn a_function_key_names_its_schema() {
1084        let error = NameOverrides::parse(r#"{"functions": {"volatile_func1": {}}}"#)
1085            .expect_err("a bare function name is not a key");
1086        assert!(error.contains("schema.function"), "{}", error);
1087    }
1088
1089    #[test]
1090    fn nothing_given_is_not_an_error() {
1091        assert!(NameOverrides::parse("").unwrap().is_empty());
1092        assert!(NameOverrides::parse("   ").unwrap().is_empty());
1093        assert!(NameOverrides::parse("{}").unwrap().is_empty());
1094    }
1095
1096    #[test]
1097    fn a_key_without_a_schema_says_so() {
1098        // The likeliest mistake, and one that would otherwise be silent: the
1099        // table is simply never found and every name is ignored.
1100        let error = NameOverrides::parse(r#"{"author": {"name": "Author"}}"#).unwrap_err();
1101        assert!(error.contains("public.author"), "unhelpful: {}", error);
1102    }
1103
1104    #[test]
1105    fn a_document_that_is_not_one_says_so() {
1106        let error = NameOverrides::parse("{not json}").unwrap_err();
1107        assert!(error.contains("valid JSON"), "unhelpful: {}", error);
1108    }
1109
1110    #[test]
1111    fn a_path_that_is_not_there_names_itself() {
1112        let error = NameOverrides::parse("/no/such/names.json").unwrap_err();
1113        assert!(
1114            error.contains("/no/such/names.json"),
1115            "unhelpful: {}",
1116            error
1117        );
1118    }
1119
1120    const GRANTED: &str = r#"{
1121        "tables": {
1122            "public.article": {
1123                "permissions": {
1124                    "user": {
1125                        "select": {
1126                            "columns": ["id", "title", "content"],
1127                            "filter": {"$or": [{"author_id": "X-HASURA-USER-ID"},
1128                                               {"is_published": true}]},
1129                            "limit": 10,
1130                            "allow_aggregations": true,
1131                            "computed_fields": ["get_articles"]
1132                        },
1133                        "insert": {
1134                            "check": {"author_id": "X-Hasura-User-Id"},
1135                            "columns": ["title", "content"],
1136                            "set": {"author_id": "X-Hasura-User-Id"},
1137                            "backend_only": true
1138                        },
1139                        "update": {"columns": ["title"], "filter": {}},
1140                        "delete": {"filter": {"is_published": false}}
1141                    },
1142                    "anonymous": {
1143                        "select": {"columns": "*", "filter": {"is_published": true}}
1144                    }
1145                }
1146            }
1147        }
1148    }"#;
1149
1150    #[test]
1151    fn what_a_role_may_do_is_read_from_the_document() {
1152        let names = NameOverrides::parse(GRANTED).unwrap();
1153        let user = names.permissions("public", "article", "user").unwrap();
1154
1155        let select = user.select.as_ref().unwrap();
1156        assert!(select.columns.allows("title"));
1157        assert!(!select.columns.allows("is_published"));
1158        assert_eq!(select.limit, Some(10));
1159        assert!(select.allow_aggregations);
1160        assert_eq!(select.computed_fields, vec!["get_articles".to_string()]);
1161        // The predicate is carried, not read: compiling it is the query
1162        // builder's job and a failure there should say so there.
1163        assert_eq!(
1164            select.filter["$or"][1]["is_published"],
1165            serde_json::json!(true)
1166        );
1167
1168        let insert = user.insert.as_ref().unwrap();
1169        assert!(insert.backend_only);
1170        assert_eq!(
1171            insert.set.get("author_id"),
1172            Some(&serde_json::json!("X-Hasura-User-Id"))
1173        );
1174
1175        assert!(user.update.is_some());
1176        assert!(user.delete.is_some());
1177    }
1178
1179    #[test]
1180    fn a_wildcard_column_set_is_not_a_list_that_happens_to_be_long() {
1181        let names = NameOverrides::parse(GRANTED).unwrap();
1182        let anonymous = names.permissions("public", "article", "anonymous").unwrap();
1183        let select = anonymous.select.as_ref().unwrap();
1184        assert_eq!(select.columns, ColumnSet::All);
1185        // Which means a column nobody has thought of yet is covered too.
1186        assert!(select.columns.allows("a_column_added_next_year"));
1187        // And this role was told nothing about writing.
1188        assert!(anonymous.insert.is_none());
1189    }
1190
1191    #[test]
1192    fn a_role_named_nothing_has_nothing() {
1193        let names = NameOverrides::parse(GRANTED).unwrap();
1194        assert!(names.permissions("public", "article", "stranger").is_none());
1195        assert!(names.permissions("public", "author", "user").is_none());
1196    }
1197
1198    #[test]
1199    fn the_roles_come_back_sorted_and_once_each() {
1200        let names = NameOverrides::parse(GRANTED).unwrap();
1201        assert_eq!(names.roles(), vec!["anonymous", "user"]);
1202        assert_eq!(names.granted(), 2);
1203    }
1204
1205    #[test]
1206    fn a_document_of_names_alone_leaves_the_layer_off() {
1207        assert!(!NameOverrides::parse(SAMPLE).unwrap().has_permissions());
1208        assert!(NameOverrides::parse(GRANTED).unwrap().has_permissions());
1209        assert!(NameOverrides::parse("").unwrap().roles().is_empty());
1210    }
1211
1212    #[test]
1213    fn a_column_set_that_is_neither_says_so() {
1214        let error = NameOverrides::parse(
1215            r#"{"tables": {"public.a": {"permissions":
1216                 {"user": {"select": {"columns": "all"}}}}}}"#,
1217        )
1218        .unwrap_err();
1219        assert!(error.contains("not a column set"), "unhelpful: {}", error);
1220    }
1221
1222    #[test]
1223    fn a_permission_survives_the_round_trip() {
1224        // The converter writes this document and the server reads it, so the
1225        // two spellings of a column set have to mean the same thing in both
1226        // directions -- `"*"` must not come back as the one-element list
1227        // `["*"]`, which would name a column no table has.
1228        let names = NameOverrides::parse(GRANTED).unwrap();
1229        let written = serde_json::to_string(&Sections {
1230            tables: names.tables.clone(),
1231            functions: HashMap::new(),
1232            introspection_disabled_for: Vec::new(),
1233        })
1234        .unwrap();
1235        let again = NameOverrides::parse(&written).unwrap();
1236        assert_eq!(again, names);
1237    }
1238}