feat(backend): migration mekanizması, indeksler ve ortak model yardımcıları
- db/migrations.rs: schema_migrations ile sürümlü migration (v1: koleksiyonlar) - db/indexes.rs: DATA_MODEL indeksleri + senkronizasyon indeksi, açılışta otomatik - models/common.rs: UUID v7, ortak alanlar, hane kapsamlı filtre - Birim testleri (4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
//! İndeksler (DATA_MODEL.md §4). Açılışta her seferinde çağrılır;
|
||||
//! MongoDB zaten var olan aynı indeksi yeniden oluşturmaz.
|
||||
|
||||
use mongodb::{
|
||||
Database, IndexModel,
|
||||
bson::{Document, doc},
|
||||
error::Result,
|
||||
options::IndexOptions,
|
||||
};
|
||||
|
||||
use super::col;
|
||||
|
||||
pub async fn ensure(db: &Database) -> Result<()> {
|
||||
for (collection, indexes) in definitions() {
|
||||
db.collection::<Document>(collection)
|
||||
.create_indexes(indexes)
|
||||
.await?;
|
||||
}
|
||||
tracing::info!("İndeksler hazır");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn idx(keys: Document) -> IndexModel {
|
||||
IndexModel::builder().keys(keys).build()
|
||||
}
|
||||
|
||||
fn idx_opts(keys: Document, options: IndexOptions) -> IndexModel {
|
||||
IndexModel::builder().keys(keys).options(options).build()
|
||||
}
|
||||
|
||||
fn unique() -> IndexOptions {
|
||||
IndexOptions::builder().unique(true).build()
|
||||
}
|
||||
|
||||
fn definitions() -> Vec<(&'static str, Vec<IndexModel>)> {
|
||||
let hp = || idx(doc! { "household_id": 1, "period_id": 1 });
|
||||
|
||||
let mut defs = vec![
|
||||
(col::USERS, vec![idx_opts(doc! { "email": 1 }, unique())]),
|
||||
(col::INVITES, vec![idx_opts(doc! { "code": 1 }, unique())]),
|
||||
(
|
||||
col::PERIODS,
|
||||
vec![
|
||||
idx(doc! { "household_id": 1, "start_date": 1 }),
|
||||
// Hanede en fazla bir açık dönem
|
||||
idx_opts(
|
||||
doc! { "household_id": 1 },
|
||||
IndexOptions::builder()
|
||||
.name("one_open_period_per_household".to_string())
|
||||
.unique(true)
|
||||
.partial_filter_expression(doc! { "status": "open" })
|
||||
.build(),
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
col::CATEGORIES,
|
||||
vec![idx(doc! { "household_id": 1, "sort_order": 1 })],
|
||||
),
|
||||
(
|
||||
col::PAYMENT_METHODS,
|
||||
vec![idx(doc! { "household_id": 1, "sort_order": 1 })],
|
||||
),
|
||||
(col::PLAN_ITEMS, vec![hp()]),
|
||||
(col::BUDGETS, vec![hp()]),
|
||||
(col::NOTES, vec![hp()]),
|
||||
(
|
||||
col::ENTRIES,
|
||||
vec![
|
||||
idx(doc! { "household_id": 1, "period_id": 1, "date": 1 }),
|
||||
idx(doc! { "household_id": 1, "date": 1 }),
|
||||
],
|
||||
),
|
||||
(
|
||||
col::CARRYOVERS,
|
||||
vec![idx(doc! { "household_id": 1, "to_period_id": 1 })],
|
||||
),
|
||||
(
|
||||
col::ASSET_TRANSACTIONS,
|
||||
vec![idx(doc! { "household_id": 1, "asset_id": 1, "date": 1 })],
|
||||
),
|
||||
(
|
||||
col::EXCHANGE_RATES,
|
||||
vec![idx(doc! { "code": 1, "household_id": 1, "rate_date": -1 })],
|
||||
),
|
||||
];
|
||||
|
||||
// Senkronizasyon (Faz 11): "şu tarihten sonra değişenler" sorgusu.
|
||||
for &name in col::HOUSEHOLD_SCOPED {
|
||||
let sync = idx(doc! { "household_id": 1, "updated_at": 1 });
|
||||
match defs.iter_mut().find(|(c, _)| *c == name) {
|
||||
Some((_, list)) => list.push(sync),
|
||||
None => defs.push((name, vec![sync])),
|
||||
}
|
||||
}
|
||||
|
||||
defs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_household_collection_has_sync_index() {
|
||||
let defs = definitions();
|
||||
for &name in col::HOUSEHOLD_SCOPED {
|
||||
let (_, list) = defs.iter().find(|(c, _)| *c == name).unwrap();
|
||||
assert!(
|
||||
list.iter()
|
||||
.any(|i| i.keys == doc! { "household_id": 1, "updated_at": 1 }),
|
||||
"{name} koleksiyonunda senkronizasyon indeksi yok"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user