Skip to content

Commit 4f8930d

Browse files
committed
Add support for single record queries by primary key
1 parent 006dd45 commit 4f8930d

13 files changed

+1393
-299
lines changed

src/builder.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,19 @@ pub struct NodeBuilder {
958958
pub selections: Vec<NodeSelection>,
959959
}
960960

961+
#[derive(Clone, Debug)]
962+
pub struct NodeByPkBuilder {
963+
// args - map of column name to value
964+
pub pk_values: HashMap<String, serde_json::Value>,
965+
966+
pub _alias: String,
967+
968+
// metadata
969+
pub table: Arc<Table>,
970+
971+
pub selections: Vec<NodeSelection>,
972+
}
973+
961974
#[derive(Clone, Debug)]
962975
pub enum NodeSelection {
963976
Connection(ConnectionBuilder),
@@ -1816,6 +1829,188 @@ where
18161829
})
18171830
}
18181831

1832+
pub fn to_node_by_pk_builder<'a, T>(
1833+
field: &__Field,
1834+
query_field: &graphql_parser::query::Field<'a, T>,
1835+
fragment_definitions: &Vec<FragmentDefinition<'a, T>>,
1836+
variables: &serde_json::Value,
1837+
variable_definitions: &Vec<VariableDefinition<'a, T>>,
1838+
) -> Result<NodeByPkBuilder, String>
1839+
where
1840+
T: Text<'a> + Eq + AsRef<str> + Clone,
1841+
T::Value: Hash,
1842+
{
1843+
let type_ = field.type_().unmodified_type();
1844+
let alias = alias_or_name(query_field);
1845+
1846+
match type_ {
1847+
__Type::Node(xtype) => {
1848+
let type_name = xtype
1849+
.name()
1850+
.ok_or("Encountered type without name in node_by_pk builder")?;
1851+
1852+
let field_map = field_map(&__Type::Node(xtype.clone()));
1853+
1854+
// Get primary key columns from the table
1855+
let pkey = xtype
1856+
.table
1857+
.primary_key()
1858+
.ok_or("Table has no primary key".to_string())?;
1859+
1860+
// Create a map of expected field arguments based on the field's arg definitions
1861+
let mut pk_arg_map = HashMap::new();
1862+
for arg in field.args() {
1863+
if let Some(NodeSQLType::Column(col)) = &arg.sql_type {
1864+
pk_arg_map.insert(arg.name().to_string(), col.name.clone());
1865+
}
1866+
}
1867+
1868+
let mut pk_values = HashMap::new();
1869+
1870+
// Process each argument in the query
1871+
for arg in &query_field.arguments {
1872+
let arg_name = arg.0.as_ref();
1873+
1874+
// Find the corresponding column name from our argument map
1875+
if let Some(col_name) = pk_arg_map.get(arg_name) {
1876+
let value = to_gson(&arg.1, variables, variable_definitions)?;
1877+
let json_value = gson::gson_to_json(&value)?;
1878+
pk_values.insert(col_name.clone(), json_value);
1879+
}
1880+
}
1881+
1882+
// Need values for all primary key columns
1883+
if pk_values.len() != pkey.column_names.len() {
1884+
return Err("All primary key columns must be provided".to_string());
1885+
}
1886+
1887+
let mut builder_fields = vec![];
1888+
let selection_fields = normalize_selection_set(
1889+
&query_field.selection_set,
1890+
fragment_definitions,
1891+
&type_name,
1892+
variables,
1893+
)?;
1894+
1895+
for selection_field in selection_fields {
1896+
match field_map.get(selection_field.name.as_ref()) {
1897+
None => {
1898+
return Err(format!(
1899+
"Unknown field '{}' on type '{}'",
1900+
selection_field.name.as_ref(),
1901+
&type_name
1902+
))
1903+
}
1904+
Some(f) => {
1905+
let alias = alias_or_name(&selection_field);
1906+
1907+
let node_selection = match &f.sql_type {
1908+
Some(node_sql_type) => match node_sql_type {
1909+
NodeSQLType::Column(col) => NodeSelection::Column(ColumnBuilder {
1910+
alias,
1911+
column: Arc::clone(col),
1912+
}),
1913+
NodeSQLType::Function(func) => {
1914+
let function_selection = match &f.type_() {
1915+
__Type::Scalar(_) => FunctionSelection::ScalarSelf,
1916+
__Type::List(_) => FunctionSelection::Array,
1917+
__Type::Node(_) => {
1918+
let node_builder = to_node_builder(
1919+
f,
1920+
&selection_field,
1921+
fragment_definitions,
1922+
variables,
1923+
&[],
1924+
variable_definitions,
1925+
)?;
1926+
FunctionSelection::Node(node_builder)
1927+
}
1928+
__Type::Connection(_) => {
1929+
let connection_builder = to_connection_builder(
1930+
f,
1931+
&selection_field,
1932+
fragment_definitions,
1933+
variables,
1934+
&[],
1935+
variable_definitions,
1936+
)?;
1937+
FunctionSelection::Connection(connection_builder)
1938+
}
1939+
_ => {
1940+
return Err(
1941+
"invalid return type from function".to_string()
1942+
)
1943+
}
1944+
};
1945+
NodeSelection::Function(FunctionBuilder {
1946+
alias,
1947+
function: Arc::clone(func),
1948+
table: Arc::clone(&xtype.table),
1949+
selection: function_selection,
1950+
})
1951+
}
1952+
NodeSQLType::NodeId(pkey_columns) => {
1953+
NodeSelection::NodeId(NodeIdBuilder {
1954+
alias,
1955+
columns: pkey_columns.clone(),
1956+
table_name: xtype.table.name.clone(),
1957+
schema_name: xtype.table.schema.clone(),
1958+
})
1959+
}
1960+
},
1961+
_ => match f.name().as_ref() {
1962+
"__typename" => NodeSelection::Typename {
1963+
alias: alias_or_name(&selection_field),
1964+
typename: xtype.name().expect("node type should have a name"),
1965+
},
1966+
_ => match f.type_().unmodified_type() {
1967+
__Type::Connection(_) => {
1968+
let con_builder = to_connection_builder(
1969+
f,
1970+
&selection_field,
1971+
fragment_definitions,
1972+
variables,
1973+
&[],
1974+
variable_definitions,
1975+
);
1976+
NodeSelection::Connection(con_builder?)
1977+
}
1978+
__Type::Node(_) => {
1979+
let node_builder = to_node_builder(
1980+
f,
1981+
&selection_field,
1982+
fragment_definitions,
1983+
variables,
1984+
&[],
1985+
variable_definitions,
1986+
);
1987+
NodeSelection::Node(node_builder?)
1988+
}
1989+
_ => {
1990+
return Err(format!(
1991+
"unexpected field type on node {}",
1992+
f.name()
1993+
));
1994+
}
1995+
},
1996+
},
1997+
};
1998+
builder_fields.push(node_selection);
1999+
}
2000+
}
2001+
}
2002+
2003+
Ok(NodeByPkBuilder {
2004+
pk_values,
2005+
_alias: alias,
2006+
table: Arc::clone(&xtype.table),
2007+
selections: builder_fields,
2008+
})
2009+
}
2010+
_ => Err("cannot build query for non-node type".to_string()),
2011+
}
2012+
}
2013+
18192014
// Introspection
18202015

18212016
#[allow(clippy::large_enum_variant)]

src/graphql.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,58 @@ impl ___Type for QueryType {
12311231
};
12321232

12331233
f.push(collection_entrypoint);
1234+
1235+
// Add single record query by primary key if the table has a primary key
1236+
if let Some(primary_key) = table.primary_key() {
1237+
let node_type = NodeType {
1238+
table: Arc::clone(table),
1239+
fkey: None,
1240+
reverse_reference: None,
1241+
schema: Arc::clone(&self.schema),
1242+
};
1243+
1244+
// Create arguments for each primary key column
1245+
let mut pk_args = Vec::new();
1246+
for col_name in &primary_key.column_names {
1247+
if let Some(col) = table.columns.iter().find(|c| &c.name == col_name) {
1248+
let col_type = sql_column_to_graphql_type(col, &self.schema)
1249+
.ok_or_else(|| {
1250+
format!(
1251+
"Could not determine GraphQL type for column {}",
1252+
col_name
1253+
)
1254+
})
1255+
.unwrap_or_else(|_| __Type::Scalar(Scalar::String(None)));
1256+
1257+
// Use graphql_column_field_name to convert snake_case to camelCase if needed
1258+
let arg_name = self.schema.graphql_column_field_name(col);
1259+
1260+
pk_args.push(__InputValue {
1261+
name_: arg_name,
1262+
type_: __Type::NonNull(NonNullType {
1263+
type_: Box::new(col_type),
1264+
}),
1265+
description: Some(format!("The record's `{}` value", col_name)),
1266+
default_value: None,
1267+
sql_type: Some(NodeSQLType::Column(Arc::clone(col))),
1268+
});
1269+
}
1270+
}
1271+
1272+
let pk_entrypoint = __Field {
1273+
name_: format!("{}ByPk", lowercase_first_letter(table_base_type_name)),
1274+
type_: __Type::Node(node_type),
1275+
args: pk_args,
1276+
description: Some(format!(
1277+
"Retrieve a record of type `{}` by its primary key",
1278+
table_base_type_name
1279+
)),
1280+
deprecation_reason: None,
1281+
sql_type: None,
1282+
};
1283+
1284+
f.push(pk_entrypoint);
1285+
}
12341286
}
12351287
}
12361288

src/resolve.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,58 @@ where
245245
Err(msg) => res_errors.push(ErrorMessage { message: msg }),
246246
}
247247
}
248+
__Type::Node(_) => {
249+
// Determine if this is a primary key query field
250+
let has_pk_args = !field_def.args().is_empty()
251+
&& field_def.args().iter().all(|arg| {
252+
// All PK field args have a SQL Column type
253+
arg.sql_type.is_some()
254+
&& matches!(
255+
arg.sql_type.as_ref().unwrap(),
256+
NodeSQLType::Column(_)
257+
)
258+
});
259+
260+
if has_pk_args {
261+
let node_by_pk_builder = to_node_by_pk_builder(
262+
field_def,
263+
selection,
264+
&fragment_definitions,
265+
variables,
266+
variable_definitions,
267+
);
268+
269+
match node_by_pk_builder {
270+
Ok(builder) => match builder.execute() {
271+
Ok(d) => {
272+
res_data[alias_or_name(selection)] = d;
273+
}
274+
Err(msg) => res_errors.push(ErrorMessage { message: msg }),
275+
},
276+
Err(msg) => res_errors.push(ErrorMessage { message: msg }),
277+
}
278+
} else {
279+
// Regular node access
280+
let node_builder = to_node_builder(
281+
field_def,
282+
selection,
283+
&fragment_definitions,
284+
variables,
285+
&[],
286+
variable_definitions,
287+
);
288+
289+
match node_builder {
290+
Ok(builder) => match builder.execute() {
291+
Ok(d) => {
292+
res_data[alias_or_name(selection)] = d;
293+
}
294+
Err(msg) => res_errors.push(ErrorMessage { message: msg }),
295+
},
296+
Err(msg) => res_errors.push(ErrorMessage { message: msg }),
297+
}
298+
}
299+
}
248300
__Type::__Type(_) => {
249301
let __type_builder = schema_type.to_type_builder(
250302
field_def,

0 commit comments

Comments
 (0)