Skip to content

Commit 6da5659

Browse files
committed
Add support for single record queries by primary key
1 parent c021f40 commit 6da5659

13 files changed

+1393
-299
lines changed

src/builder.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,19 @@ pub struct NodeBuilder {
991991
pub selections: Vec<NodeSelection>,
992992
}
993993

994+
#[derive(Clone, Debug)]
995+
pub struct NodeByPkBuilder {
996+
// args - map of column name to value
997+
pub pk_values: HashMap<String, serde_json::Value>,
998+
999+
pub _alias: String,
1000+
1001+
// metadata
1002+
pub table: Arc<Table>,
1003+
1004+
pub selections: Vec<NodeSelection>,
1005+
}
1006+
9941007
#[derive(Clone, Debug)]
9951008
pub enum NodeSelection {
9961009
Connection(ConnectionBuilder),
@@ -2028,6 +2041,188 @@ where
20282041
})
20292042
}
20302043

2044+
pub fn to_node_by_pk_builder<'a, T>(
2045+
field: &__Field,
2046+
query_field: &graphql_parser::query::Field<'a, T>,
2047+
fragment_definitions: &Vec<FragmentDefinition<'a, T>>,
2048+
variables: &serde_json::Value,
2049+
variable_definitions: &Vec<VariableDefinition<'a, T>>,
2050+
) -> Result<NodeByPkBuilder, String>
2051+
where
2052+
T: Text<'a> + Eq + AsRef<str> + Clone,
2053+
T::Value: Hash,
2054+
{
2055+
let type_ = field.type_().unmodified_type();
2056+
let alias = alias_or_name(query_field);
2057+
2058+
match type_ {
2059+
__Type::Node(xtype) => {
2060+
let type_name = xtype
2061+
.name()
2062+
.ok_or("Encountered type without name in node_by_pk builder")?;
2063+
2064+
let field_map = field_map(&__Type::Node(xtype.clone()));
2065+
2066+
// Get primary key columns from the table
2067+
let pkey = xtype
2068+
.table
2069+
.primary_key()
2070+
.ok_or("Table has no primary key".to_string())?;
2071+
2072+
// Create a map of expected field arguments based on the field's arg definitions
2073+
let mut pk_arg_map = HashMap::new();
2074+
for arg in field.args() {
2075+
if let Some(NodeSQLType::Column(col)) = &arg.sql_type {
2076+
pk_arg_map.insert(arg.name().to_string(), col.name.clone());
2077+
}
2078+
}
2079+
2080+
let mut pk_values = HashMap::new();
2081+
2082+
// Process each argument in the query
2083+
for arg in &query_field.arguments {
2084+
let arg_name = arg.0.as_ref();
2085+
2086+
// Find the corresponding column name from our argument map
2087+
if let Some(col_name) = pk_arg_map.get(arg_name) {
2088+
let value = to_gson(&arg.1, variables, variable_definitions)?;
2089+
let json_value = gson::gson_to_json(&value)?;
2090+
pk_values.insert(col_name.clone(), json_value);
2091+
}
2092+
}
2093+
2094+
// Need values for all primary key columns
2095+
if pk_values.len() != pkey.column_names.len() {
2096+
return Err("All primary key columns must be provided".to_string());
2097+
}
2098+
2099+
let mut builder_fields = vec![];
2100+
let selection_fields = normalize_selection_set(
2101+
&query_field.selection_set,
2102+
fragment_definitions,
2103+
&type_name,
2104+
variables,
2105+
)?;
2106+
2107+
for selection_field in selection_fields {
2108+
match field_map.get(selection_field.name.as_ref()) {
2109+
None => {
2110+
return Err(format!(
2111+
"Unknown field '{}' on type '{}'",
2112+
selection_field.name.as_ref(),
2113+
&type_name
2114+
))
2115+
}
2116+
Some(f) => {
2117+
let alias = alias_or_name(&selection_field);
2118+
2119+
let node_selection = match &f.sql_type {
2120+
Some(node_sql_type) => match node_sql_type {
2121+
NodeSQLType::Column(col) => NodeSelection::Column(ColumnBuilder {
2122+
alias,
2123+
column: Arc::clone(col),
2124+
}),
2125+
NodeSQLType::Function(func) => {
2126+
let function_selection = match &f.type_() {
2127+
__Type::Scalar(_) => FunctionSelection::ScalarSelf,
2128+
__Type::List(_) => FunctionSelection::Array,
2129+
__Type::Node(_) => {
2130+
let node_builder = to_node_builder(
2131+
f,
2132+
&selection_field,
2133+
fragment_definitions,
2134+
variables,
2135+
&[],
2136+
variable_definitions,
2137+
)?;
2138+
FunctionSelection::Node(node_builder)
2139+
}
2140+
__Type::Connection(_) => {
2141+
let connection_builder = to_connection_builder(
2142+
f,
2143+
&selection_field,
2144+
fragment_definitions,
2145+
variables,
2146+
&[],
2147+
variable_definitions,
2148+
)?;
2149+
FunctionSelection::Connection(connection_builder)
2150+
}
2151+
_ => {
2152+
return Err(
2153+
"invalid return type from function".to_string()
2154+
)
2155+
}
2156+
};
2157+
NodeSelection::Function(FunctionBuilder {
2158+
alias,
2159+
function: Arc::clone(func),
2160+
table: Arc::clone(&xtype.table),
2161+
selection: function_selection,
2162+
})
2163+
}
2164+
NodeSQLType::NodeId(pkey_columns) => {
2165+
NodeSelection::NodeId(NodeIdBuilder {
2166+
alias,
2167+
columns: pkey_columns.clone(),
2168+
table_name: xtype.table.name.clone(),
2169+
schema_name: xtype.table.schema.clone(),
2170+
})
2171+
}
2172+
},
2173+
_ => match f.name().as_ref() {
2174+
"__typename" => NodeSelection::Typename {
2175+
alias: alias_or_name(&selection_field),
2176+
typename: xtype.name().expect("node type should have a name"),
2177+
},
2178+
_ => match f.type_().unmodified_type() {
2179+
__Type::Connection(_) => {
2180+
let con_builder = to_connection_builder(
2181+
f,
2182+
&selection_field,
2183+
fragment_definitions,
2184+
variables,
2185+
&[],
2186+
variable_definitions,
2187+
);
2188+
NodeSelection::Connection(con_builder?)
2189+
}
2190+
__Type::Node(_) => {
2191+
let node_builder = to_node_builder(
2192+
f,
2193+
&selection_field,
2194+
fragment_definitions,
2195+
variables,
2196+
&[],
2197+
variable_definitions,
2198+
);
2199+
NodeSelection::Node(node_builder?)
2200+
}
2201+
_ => {
2202+
return Err(format!(
2203+
"unexpected field type on node {}",
2204+
f.name()
2205+
));
2206+
}
2207+
},
2208+
},
2209+
};
2210+
builder_fields.push(node_selection);
2211+
}
2212+
}
2213+
}
2214+
2215+
Ok(NodeByPkBuilder {
2216+
pk_values,
2217+
_alias: alias,
2218+
table: Arc::clone(&xtype.table),
2219+
selections: builder_fields,
2220+
})
2221+
}
2222+
_ => Err("cannot build query for non-node type".to_string()),
2223+
}
2224+
}
2225+
20312226
// Introspection
20322227

20332228
#[allow(clippy::large_enum_variant)]

src/graphql.rs

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

12491249
f.push(collection_entrypoint);
1250+
1251+
// Add single record query by primary key if the table has a primary key
1252+
if let Some(primary_key) = table.primary_key() {
1253+
let node_type = NodeType {
1254+
table: Arc::clone(table),
1255+
fkey: None,
1256+
reverse_reference: None,
1257+
schema: Arc::clone(&self.schema),
1258+
};
1259+
1260+
// Create arguments for each primary key column
1261+
let mut pk_args = Vec::new();
1262+
for col_name in &primary_key.column_names {
1263+
if let Some(col) = table.columns.iter().find(|c| &c.name == col_name) {
1264+
let col_type = sql_column_to_graphql_type(col, &self.schema)
1265+
.ok_or_else(|| {
1266+
format!(
1267+
"Could not determine GraphQL type for column {}",
1268+
col_name
1269+
)
1270+
})
1271+
.unwrap_or_else(|_| __Type::Scalar(Scalar::String(None)));
1272+
1273+
// Use graphql_column_field_name to convert snake_case to camelCase if needed
1274+
let arg_name = self.schema.graphql_column_field_name(col);
1275+
1276+
pk_args.push(__InputValue {
1277+
name_: arg_name,
1278+
type_: __Type::NonNull(NonNullType {
1279+
type_: Box::new(col_type),
1280+
}),
1281+
description: Some(format!("The record's `{}` value", col_name)),
1282+
default_value: None,
1283+
sql_type: Some(NodeSQLType::Column(Arc::clone(col))),
1284+
});
1285+
}
1286+
}
1287+
1288+
let pk_entrypoint = __Field {
1289+
name_: format!("{}ByPk", lowercase_first_letter(table_base_type_name)),
1290+
type_: __Type::Node(node_type),
1291+
args: pk_args,
1292+
description: Some(format!(
1293+
"Retrieve a record of type `{}` by its primary key",
1294+
table_base_type_name
1295+
)),
1296+
deprecation_reason: None,
1297+
sql_type: None,
1298+
};
1299+
1300+
f.push(pk_entrypoint);
1301+
}
12501302
}
12511303
}
12521304

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)