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