use crate::api::composite::Graph;
use crate::auth::Authenticator;
use crate::client::ForceClient;
use crate::error::{ForceError, Result};
use crate::types::describe::{FieldType, SObjectDescribe};
use super::data_faker::generate_mock_record;
#[derive(Debug)]
pub struct RelationalSeeder<'a, A: Authenticator> {
client: &'a ForceClient<A>,
}
impl<'a, A: Authenticator> RelationalSeeder<'a, A> {
#[must_use]
pub fn new(client: &'a ForceClient<A>) -> Self {
Self { client }
}
pub async fn seed_hierarchy(
&self,
parent_describe: &SObjectDescribe,
child_describe: &SObjectDescribe,
child_count: usize,
) -> Result<()> {
let relationship_field = child_describe.fields.iter().find(|f| {
f.type_ == FieldType::Reference && f.reference_to.contains(&parent_describe.name)
});
let Some(rel_field) = relationship_field else {
return Err(ForceError::InvalidInput(format!(
"No reference field found on {} pointing to {}",
child_describe.name, parent_describe.name
)));
};
let mut parent_record = generate_mock_record(parent_describe);
let parent_ref_id = format!("ref_{}", parent_describe.name.to_lowercase());
let mut graph = Graph::new("SeedGraph");
parent_record.fields.remove("Id");
let parent_value = serde_json::to_value(&parent_record.fields)
.map_err(|e| ForceError::Serialization(crate::error::SerializationError::Json(e)))?;
graph = graph.post(&parent_describe.name, parent_value, &parent_ref_id)?;
let parent_id_ref_str = format!("@{{ {}.id }}", parent_ref_id).replace(' ', "");
for i in 0..child_count {
let mut child_record = generate_mock_record(child_describe);
child_record.fields.remove("Id");
child_record.fields.insert(
rel_field.name.clone(),
serde_json::Value::String(parent_id_ref_str.clone()),
);
let child_ref_id = format!("ref_{}_{}", child_describe.name.to_lowercase(), i);
let child_value = serde_json::to_value(&child_record.fields).map_err(|e| {
ForceError::Serialization(crate::error::SerializationError::Json(e))
})?;
graph = graph.post(&child_describe.name, child_value, &child_ref_id)?;
}
let mut graph_req = self.client.composite().graph();
graph_req = graph_req.add_graph(graph)?;
let response = graph_req.execute().await?;
for res in response.graphs {
if !res.is_successful {
return Err(ForceError::InvalidInput("Seed operation failed".into()));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::builder;
use crate::test_utils::mock_auth::MockAuthenticator;
use crate::test_utils::must::Must;
use serde_json::json;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn create_mock_server() -> MockServer {
MockServer::start().await
}
async fn create_test_client(mock_server: &MockServer) -> ForceClient<MockAuthenticator> {
let auth = MockAuthenticator::new("test_token", &mock_server.uri());
builder().authenticate(auth).build().await.must()
}
fn create_mock_describe(name: &str, fields_json: &serde_json::Value) -> SObjectDescribe {
let describe_json = json!({
"name": name,
"label": name,
"custom": false,
"queryable": true,
"activateable": false, "createable": true, "customSetting": false, "deletable": true,
"deprecatedAndHidden": false, "feedEnabled": true, "hasSubtypes": false,
"isSubtype": false, "keyPrefix": "001", "labelPlural": format!("{}s", name), "layoutable": true,
"mergeable": true, "mruEnabled": true, "replicateable": true, "retrieveable": true,
"searchable": true, "triggerable": true, "undeletable": true, "updateable": true,
"urls": {}, "childRelationships": [], "recordTypeInfos": [],
"fields": fields_json.clone()
});
serde_json::from_value(describe_json).must()
}
fn mock_field(name: &str, field_type: &str, ref_to: &[&str]) -> serde_json::Value {
json!({
"name": name,
"type": field_type,
"label": format!("{} Label", name),
"createable": true,
"autoNumber": false, "calculated": false,
"aggregatable": true, "byteLength": 18,
"cascadeDelete": false, "caseSensitive": false, "custom": false,
"defaultedOnCreate": true, "dependentPicklist": false, "deprecatedAndHidden": false,
"digits": 0, "displayLocationInDecimal": false, "encrypted": false, "externalId": false,
"filterable": true, "groupable": true, "highScaleNumber": false, "htmlFormatted": false,
"idLookup": true, "length": 18, "nameField": false, "namePointing": false, "nillable": false,
"permissionable": false, "polymorphicForeignKey": false, "precision": 0, "queryByDistance": false,
"restrictedDelete": false, "restrictedPicklist": false, "scale": 0, "soapType": "xsd:string",
"sortable": true, "unique": false, "updateable": false, "writeRequiresMasterRead": false,
"referenceTo": ref_to
})
}
#[tokio::test]
async fn test_seed_hierarchy_success() {
let mock_server = create_mock_server().await;
let client = create_test_client(&mock_server).await;
let parent_describe =
create_mock_describe("Account", &json!([mock_field("Name", "string", &[])]));
let child_describe = create_mock_describe(
"Contact",
&json!([
mock_field("LastName", "string", &[]),
mock_field("AccountId", "reference", &["Account"])
]),
);
Mock::given(method("POST"))
.and(path("/services/data/v67.0/composite/graph"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"graphs": [
{
"graphId": "SeedGraph",
"isSuccessful": true,
"compositeResponse": [
{
"body": { "id": "001000000000001AAA", "success": true, "errors": [] },
"httpHeaders": {}, "httpStatusCode": 201, "referenceId": "ref_account"
},
{
"body": { "id": "003000000000001AAA", "success": true, "errors": [] },
"httpHeaders": {}, "httpStatusCode": 201, "referenceId": "ref_contact_0"
}
]
}
]
})))
.mount(&mock_server)
.await;
let seeder = RelationalSeeder::new(&client);
let result = seeder
.seed_hierarchy(&parent_describe, &child_describe, 1)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_seed_hierarchy_missing_relationship() {
let mock_server = create_mock_server().await;
let client = create_test_client(&mock_server).await;
let parent_describe =
create_mock_describe("Account", &json!([mock_field("Name", "string", &[])]));
let child_describe = create_mock_describe(
"Contact",
&json!([
mock_field("LastName", "string", &[]),
mock_field("OpportunityId", "reference", &["Opportunity"])
]),
);
let seeder = RelationalSeeder::new(&client);
let result = seeder
.seed_hierarchy(&parent_describe, &child_describe, 1)
.await;
assert!(matches!(result, Err(ForceError::InvalidInput(_))));
if let Err(ForceError::InvalidInput(msg)) = result {
assert!(msg.contains("No reference field found"));
}
}
}