Adding a New Entity
You have an Ontogen project with a working build.rs pipeline. Now you need a new entity. This recipe walks through the process from schema file to working CRUD with hooks.
We’ll add an Exercise entity to a fitness tracking app — something with a couple of plain fields, an optional field, and no relations (we’ll keep it simple).
-
Create the schema file
Section titled “Create the schema file”Create
src/schema/exercise.rs:use ontogen_macros::OntologyEntity;use serde::{Deserialize, Serialize};#[derive(Debug, Clone, Serialize, Deserialize, OntologyEntity)]#[ontology(entity, table = "exercises")]pub struct Exercise {#[ontology(id)]pub id: String,pub name: String,/// "chest" | "back" | "legs" | "shoulders" | "arms" | "core"pub muscle_group: String,/// "barbell" | "dumbbell" | "machine" | "bodyweight" | "cable"pub equipment: String,#[serde(default)]pub notes: Option<String>,}The key pieces:
#[derive(OntologyEntity)]makes#[ontology(...)]attributes legal.#[ontology(entity, table = "exercises")]registers this as an entity with a specific table name.#[ontology(id)]marks the primary key.#[serde(default)]on optional fields means they deserialize asNonewhen absent.
-
Register it in
Section titled “Register it in mod.rs”mod.rsAdd the new module and re-export in
src/schema/mod.rs:mod exercise;pub use exercise::Exercise;If you’re using generated DTOs, also add the re-exports (these files don’t exist yet — they’ll be generated in the next step):
pub use dto::exercise::{CreateExerciseInput, UpdateExerciseInput}; -
Run
cargo build. Ontogen detects the new schema file and generates code for your entity through every pipeline stage you’ve configured.Terminal window cargo buildWatch the build output. If something’s wrong, you’ll see
cargo:warning=ontogen:lines with specific error messages. -
Verify generated files
Section titled “Verify generated files”After the build, check what was generated. Depending on your pipeline configuration, you should see new files in several
generated/directories:SeaORM entity (if
gen_seaormis configured):src/persistence/db/entities/generated/exercise.rsContains the
Modelstruct with#[sea_orm(...)]attributes, aRelationenum, andActiveModelBehavior.Conversions (if
gen_seaormis configured):src/persistence/db/conversions/generated/exercise.rsContains
Exercise::from_model()andExercise::to_active_model().DTOs (if
gen_dtosorgen_storeis configured):src/schema/dto/exercise.rsContains
CreateExerciseInputandUpdateExerciseInput.Store CRUD (if
gen_storeis configured):src/store/generated/exercise.rsContains
list_exercises(),get_exercise(),create_exercise(),update_exercise(),delete_exercise(), and anExerciseUpdatestruct.Hooks (if
gen_storewithhooks_diris configured):src/store/hooks/exercise.rsContains empty
before_create,after_create,before_update,after_update,before_delete,after_deletefunctions.API module (if
gen_apiis configured):src/api/v1/generated/exercise.rsContains
list(),get_by_id(),create(),update(),delete()forwarding functions.Transports (if
gen_serversis configured): The generated transport files (http/generated.rs,ipc/generated.rs) are updated to include handlers for the new entity. -
Add the error variant
Section titled “Add the error variant”Generated store code uses an
AppErrorenum with entity-specificNotFoundvariants. You need to add one for the new entity. In yourAppErrordefinition (typically insrc/schema/mod.rs):#[derive(Debug, thiserror::Error)]pub enum AppError {// ... existing variants ...#[error("Exercise not found: {0}")]ExerciseNotFound(String),}The generated store code references
AppError::ExerciseNotFound, so this must match. -
Add hook logic (optional)
Section titled “Add hook logic (optional)”Open the scaffolded hook file at
src/store/hooks/exercise.rs. It has empty function bodies:pub async fn before_create(_store: &Store,_exercise: &mut Exercise,) -> Result<(), AppError> {Ok(())}Fill in whatever validation or side effects you need. For example, normalizing the muscle group to lowercase:
pub async fn before_create(_store: &Store,exercise: &mut Exercise,) -> Result<(), AppError> {exercise.muscle_group = exercise.muscle_group.to_lowercase();Ok(())}Remember: hook files are never overwritten by the generator. They’re yours.
-
Create the database table
Section titled “Create the database table”Ontogen generates Rust code, not SQL migrations. You need to create the table yourself. Using SeaORM migrations, or just raw SQL for development:
CREATE TABLE exercises (id TEXT PRIMARY KEY,name TEXT NOT NULL,muscle_group TEXT NOT NULL,equipment TEXT NOT NULL,notes TEXT);The column types should match the field type mappings.
-
Build again to make sure everything compiles:
Terminal window cargo buildIf you have integration tests, the new CRUD methods are available on your
Store:let exercise = Exercise {id: "ex-bench-press".to_string(),name: "Bench Press".to_string(),muscle_group: "chest".to_string(),equipment: "barbell".to_string(),notes: None,};let created = store.create_exercise(exercise).await?;assert_eq!(created.name, "Bench Press");
Adding relations later
Section titled “Adding relations later”If you later need to add a relation to an existing entity (say, exercises have tags), edit the schema file and add the relation annotation:
#[serde(default)]#[ontology(relation(many_to_many, target = "Tag"))]pub tags: Vec<String>,Rebuild, and the generated code updates everywhere:
- A new
exercise_tagsjunction table entity appears. - Store CRUD gains
sync_junction()calls in create/update andpopulate_exercise_relations()for reads. - DTOs include the
tagsfield. - Transports pass the tags through.
You’ll need to create the junction table in your database:
CREATE TABLE exercise_tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, exercise_id TEXT NOT NULL REFERENCES exercises(id), tag_id TEXT NOT NULL REFERENCES tags(id));Checklist
Section titled “Checklist”When you add a new entity, make sure you’ve done all of these:
- Created
src/schema/entity_name.rswith#[derive(OntologyEntity)] - Added
mod entity_name;andpub useinsrc/schema/mod.rs - Added DTO re-exports in
src/schema/mod.rs(if using DTOs) - Added
AppError::EntityNameNotFoundvariant - Added
EntityKind::EntityNamevariant (if using change events) - Created the database table (migration or raw SQL)
- Built successfully with
cargo build - Filled in hook logic as needed