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