Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,183 @@ The default order of results is defined by the underlying table's primary key co

Note, only one key value pair may be provided to each element of the input array. For example, `[{name: AscNullsLast}, {id: AscNullFirst}]` is valid. Passing multiple key value pairs in a single element of the input array e.g. `[{name: AscNullsLast, id: AscNullFirst}]`, is invalid.

### Primary Key Queries

Each table has a top level field in the `Query` type for selecting a single record by primary key from that table. The field is named `<table>ByPk`

**SQL Setup**
```sql
create table "Blog"(
id serial primary key,
name varchar(255) not null,
description varchar(255),
"createdAt" timestamp not null,
"updatedAt" timestamp not null
);
```

**GraphQL Types**
=== "QueryType"

```graphql
"""The root type for querying data"""
type Query {

"""Retrieve a blog by its id"""
blogByPk(id: Int!): Blog

}
```

To query the table by primary key, pass the value of the primary key field to the field:

**Example**
=== "Query"

```graphql
{
blogByPk(
id: 1
) {
id
name
description
}
}
```

=== "Response"

```json
{
"data": {
"blogByPk": {
"id": 1,
"name": "Some Blog",
"description": "Description of Some Blog"
}
}
}
```

If a record with the give id doesn't exist, the field will return null:

**Example**
=== "Query"

```graphql
{
blogByPk(
id: 999
) {
id
name
description
}
}
```

=== "Response"

```json
{
"data": {
"blogByPk": null
}
}
```

If the key is a composite primary key, all the columns of the primary key should be sent in the query:



**SQL Setup**
```sql
create table item(
item_id int,
product_id int,
quantity int,
price numeric(10,2),
primary key(item_id, product_id)
);
```

**GraphQL Types**
=== "QueryType"

```graphql
"""The root type for querying data"""
type Query {

"""Retrieve an item by its item and product ids"""
itemByPk(itemId: Int!, productId: Int!): Item

}
```
**Example**
=== "Query"

```graphql
{
itemByPk(
itemId: 1, productId: 2
) {
itemId
productId
quantity
price
}
}
```

=== "Response"

```json
{
"data": {
"itemByPk": {
"itemId": 1,
"productId": 2,
"quantity": 1,
"price": "24.99"
}
}
}
```

Otherwise an error will be returned:

**Example**
=== "Query"

```graphql
{
itemByPk(
itemId: 1
) {
itemId
productId
quantity
price
}
}
```

=== "Response"

```json
{
"data": null,
"errors": [
{
"message": "Missing primary key column(s): product_id"
}
]
}
```



## MutationType

The `Mutation` type is the entrypoint for mutations/edits.
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@
- feature: Add support for Postgres 18

## master

- feature: Add support for single record queries by primary key
195 changes: 195 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,13 @@ pub struct NodeBuilder {
pub selections: Vec<NodeSelection>,
}

#[derive(Clone, Debug)]
pub struct NodeByPkBuilder {
pub pk_values: HashMap<String, serde_json::Value>,
pub table: Arc<Table>,
pub selections: Vec<NodeSelection>,
}

#[derive(Clone, Debug)]
pub enum NodeSelection {
Connection(ConnectionBuilder),
Expand Down Expand Up @@ -2156,6 +2163,194 @@ where
})
}

pub fn to_node_by_pk_builder<'a, T>(
field: &__Field,
query_field: &graphql_parser::query::Field<'a, T>,
fragment_definitions: &Vec<FragmentDefinition<'a, T>>,
variables: &serde_json::Value,
variable_definitions: &Vec<VariableDefinition<'a, T>>,
) -> GraphQLResult<NodeByPkBuilder>
where
T: Text<'a> + Eq + AsRef<str> + Clone,
T::Value: Hash,
{
let type_ = field.type_().unmodified_type();

// This function is only called for Node types from resolve_selection_set
let xtype = match type_ {
__Type::Node(node_type) => node_type,
_ => return Err(GraphQLError::internal("to_node_by_pk_builder called with non-Node type")),
};

let type_name = xtype
.name()
.ok_or_else(|| GraphQLError::internal("Encountered type without name in node_by_pk builder"))?;

let field_map = field_map(&__Type::Node(xtype.clone()));

// Get primary key columns from the table
let pkey = xtype
.table
.primary_key()
.ok_or_else(|| GraphQLError::validation("Table has no primary key"))?;

// Create a map of expected field arguments based on the field's arg definitions
let mut pk_arg_map = HashMap::new();
for arg in field.args() {
if let Some(NodeSQLType::Column(col)) = &arg.sql_type {
pk_arg_map.insert(arg.name().to_string(), col.name.clone());
}
}

let mut pk_values = HashMap::new();

// Process each argument in the query
for arg in &query_field.arguments {
let arg_name = arg.0.as_ref();

// Find the corresponding column name from our argument map
if let Some(col_name) = pk_arg_map.get(arg_name) {
let value = to_gson(&arg.1, variables, variable_definitions)?;
let json_value = gson::gson_to_json(&value)?;
pk_values.insert(col_name.clone(), json_value);
}
}

// Need values for all primary key columns
if pk_values.len() != pkey.column_names.len() {
let missing_cols: Vec<_> = pkey
.column_names
.iter()
.filter(|col| !pk_values.contains_key(*col))
.collect();
return Err(GraphQLError::argument(format!(
"Missing primary key column(s): {}",
missing_cols.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
)));
}

let mut builder_fields = vec![];
let selection_fields = normalize_selection_set(
&query_field.selection_set,
fragment_definitions,
&type_name,
variables,
)?;

for selection_field in selection_fields {
match field_map.get(selection_field.name.as_ref()) {
None => {
return Err(GraphQLError::field_not_found(
selection_field.name.as_ref(),
&type_name
))
}
Some(f) => {
let alias = alias_or_name(&selection_field);

let node_selection = match &f.sql_type {
Some(node_sql_type) => match node_sql_type {
NodeSQLType::Column(col) => NodeSelection::Column(ColumnBuilder {
alias,
column: Arc::clone(col),
}),
NodeSQLType::Function(func) => {
let function_selection = match &f.type_() {
__Type::Scalar(_) => FunctionSelection::ScalarSelf,
__Type::List(_) => FunctionSelection::Array,
__Type::Node(_) => {
let node_builder = to_node_builder(
f,
&selection_field,
fragment_definitions,
variables,
&[],
variable_definitions,
)?;
FunctionSelection::Node(node_builder)
}
__Type::Connection(_) => {
let connection_builder = to_connection_builder(
f,
&selection_field,
fragment_definitions,
variables,
&[],
variable_definitions,
)?;
FunctionSelection::Connection(connection_builder)
}
_ => {
return Err(GraphQLError::type_error(
"invalid return type from function"
))
}
};
NodeSelection::Function(FunctionBuilder {
alias,
function: Arc::clone(func),
table: Arc::clone(&xtype.table),
selection: function_selection,
})
}
NodeSQLType::NodeId(pkey_columns) => {
NodeSelection::NodeId(NodeIdBuilder {
alias,
columns: pkey_columns.clone(),
table_name: xtype.table.name.clone(),
schema_name: xtype.table.schema.clone(),
})
}
},
_ => match f.name().as_ref() {
"__typename" => NodeSelection::Typename {
alias: alias_or_name(&selection_field),
typename: xtype.name().expect("node type should have a name"),
},
_ => match f.type_().unmodified_type() {
__Type::Connection(_) => {
let con_builder = to_connection_builder(
f,
&selection_field,
fragment_definitions,
variables,
&[],
variable_definitions,
);
NodeSelection::Connection(con_builder?)
}
__Type::Node(_) => {
let node_builder = to_node_builder(
f,
&selection_field,
fragment_definitions,
variables,
&[],
variable_definitions,
);
NodeSelection::Node(node_builder?)
}
_ => {
return Err(GraphQLError::type_error(format!(
"unexpected field type on node {}",
f.name()
)));
}
},
},
};
builder_fields.push(node_selection);
}
}
}

Ok(NodeByPkBuilder {
pk_values,
table: Arc::clone(&xtype.table),
selections: builder_fields,
})
}

// Introspection

#[allow(clippy::large_enum_variant)]
Expand Down
Loading