pub trait ModelRepository: Send + Sync {
// Required methods
fn list<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Model>, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
fn get_by_id<'life0, 'async_trait>(
&'life0 self,
id: i64,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
fn get_by_name<'life0, 'life1, 'async_trait>(
&'life0 self,
name: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn insert<'life0, 'life1, 'async_trait>(
&'life0 self,
model: &'life1 NewModel,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn find_by_path<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn update<'life0, 'life1, 'async_trait>(
&'life0 self,
model: &'life1 Model,
) -> Pin<Box<dyn Future<Output = Result<(), RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn delete<'life0, 'async_trait>(
&'life0 self,
id: i64,
) -> Pin<Box<dyn Future<Output = Result<(), RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
// Provided method
fn get_by_identifier<'life0, 'life1, 'async_trait>(
&'life0 self,
identifier: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait { ... }
}Expand description
Repository for model persistence operations.
This trait defines CRUD operations for models. Implementations are responsible for all storage details (SQL, filesystem, etc.).
§Design Rules
- No
sqlxtypes in signatures - CRUD-only: list, get, insert, update, delete
- Tags and search logic belong in
ModelService, not here - The one exception is
ModelRepository::get_by_identifier, a provided method: identifier resolution is a lookup-key policy, and it lives here precisely so that every facade over the repository shares one copy of it.
Required Methods§
Sourcefn list<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Model>, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn list<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Model>, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
List all models in the repository.
Sourcefn get_by_id<'life0, 'async_trait>(
&'life0 self,
id: i64,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_by_id<'life0, 'async_trait>(
&'life0 self,
id: i64,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Get a model by its database ID.
Returns Err(RepositoryError::NotFound) if the model doesn’t exist.
Sourcefn get_by_name<'life0, 'life1, 'async_trait>(
&'life0 self,
name: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_by_name<'life0, 'life1, 'async_trait>(
&'life0 self,
name: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Get a model by its name.
Returns Err(RepositoryError::NotFound) if no model with that name exists.
Sourcefn insert<'life0, 'life1, 'async_trait>(
&'life0 self,
model: &'life1 NewModel,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn insert<'life0, 'life1, 'async_trait>(
&'life0 self,
model: &'life1 NewModel,
) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Insert a new model into the repository, or update the existing row for the same model.
This upserts. Registering the same model twice is not an error: it overwrites the mutable columns and returns the existing row, id included. That is deliberate — post-download registration has to be safe to retry, and a re-scan must not fail on what it already knows.
It also means this method never reports a duplicate. A caller for whom
“already there” is an error — an explicit “add this file to my
library” rather than a registration — must ask Self::find_by_path
first. ModelService::import_from_file does.
This doc used to promise Err(RepositoryError::AlreadyExists) on a
duplicate file path. No implementation ever did that, and the promise
is what made the silent overwrite hard to see.
Sourcefn find_by_path<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn find_by_path<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Find the model registered under path, if there is one.
path is matched against the form the implementation stores.
An implementation that also stores a model’s sibling shard paths must
match those too: adding shard 2 of a group already in the library is
the same duplicate as adding shard 1, and only shard 1’s path lives in
the primary path field. SqliteModelRepository in gglib-db does
this; the test doubles in this workspace store no siblings and so match
on the single path alone, which means a sharded duplicate is only
observable in tests that use the real repository.
Callers pass a path already resolved by
canonical_model_path; this
method does not resolve it for them. That is the whole point of the
split: resolution can fail, and a failure has to reach the caller as
an error rather than decay into Ok(None), which reads as “no
duplicate” and silently reinstates the overwrite this lookup exists to
prevent.
Required rather than provided. A default body here would have to touch
the filesystem, which ports/ does not do (see the design rules in
this module’s README), and would scan the entire table per call in a
trait that every test double inherits.
Sourcefn update<'life0, 'life1, 'async_trait>(
&'life0 self,
model: &'life1 Model,
) -> Pin<Box<dyn Future<Output = Result<(), RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn update<'life0, 'life1, 'async_trait>(
&'life0 self,
model: &'life1 Model,
) -> Pin<Box<dyn Future<Output = Result<(), RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Update an existing model.
Returns Err(RepositoryError::NotFound) if the model doesn’t exist.
Provided Methods§
Sourcefn get_by_identifier<'life0, 'life1, 'async_trait>(
&'life0 self,
identifier: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_by_identifier<'life0, 'life1, 'async_trait>(
&'life0 self,
identifier: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Resolve a model by user-facing identifier: numeric database id first, then exact name.
This is the single lookup-key policy for the workspace — every
facade over a repository (ModelService, the ModelCatalogPort
adapter) delegates here rather than choosing its own key. Before this
existed the two disagreed: the service resolved ids, the catalog port
did not, so the same string resolved differently depending on which
pipeline a request travelled down.
Provided rather than required so implementors and test doubles inherit it automatically.
Returns Ok(None) when nothing matches. A storage failure on the id
lookup propagates rather than silently falling through to the name
lookup — only a genuine NotFound continues.