initial seed

This commit is contained in:
Priec
2026-06-17 13:40:21 +02:00
parent b88c990873
commit 95f195a204
6 changed files with 195 additions and 51 deletions

View File

@@ -60,7 +60,6 @@ impl Hooks for App {
Ok(vec![ Ok(vec![
Box::new(initializers::view_engine::ViewEngineInitializer), Box::new(initializers::view_engine::ViewEngineInitializer),
Box::new(initializers::admin_seeder::AdminSeeder), Box::new(initializers::admin_seeder::AdminSeeder),
Box::new(initializers::shipping_seeder::ShippingSeeder),
]) ])
} }
@@ -111,6 +110,7 @@ impl Hooks for App {
async fn seed(ctx: &AppContext, base: &Path) -> Result<()> { async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {
db::seed::<users::ActiveModel>(&ctx.db, &base.join("users.yaml").display().to_string()) db::seed::<users::ActiveModel>(&ctx.db, &base.join("users.yaml").display().to_string())
.await?; .await?;
crate::seed::seed_catalog(ctx).await?;
Ok(()) Ok(())
} }
} }

View File

@@ -1,5 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use loco_rs::prelude::*; use loco_rs::prelude::*;
use loco_rs::hash;
use sea_orm::{ActiveModelTrait, IntoActiveModel, Set};
use crate::models::users::{self, RegisterParams}; use crate::models::users::{self, RegisterParams};
@@ -18,7 +20,19 @@ impl Initializer for AdminSeeder {
if email.is_empty() || password.is_empty() { if email.is_empty() || password.is_empty() {
tracing::warn!("ADMIN_EMAIL / ADMIN_PASSWORD not set in .env; admin not seeded"); tracing::warn!("ADMIN_EMAIL / ADMIN_PASSWORD not set in .env; admin not seeded");
} else if users::Model::find_by_email(&ctx.db, &email).await.is_err() { return Ok(());
}
if let Ok(user) = users::Model::find_by_email(&ctx.db, &email).await {
// User exists — update password so .env is always the source of truth.
let hash = hash::hash_password(&password)
.map_err(|e| Error::Message(e.to_string()))?;
let mut am = user.into_active_model();
am.password = Set(hash);
am.name = Set(name);
am.update(&ctx.db).await?;
tracing::info!(admin = %email, "admin password synced from .env");
} else {
users::Model::create_with_password( users::Model::create_with_password(
&ctx.db, &ctx.db,
&RegisterParams { &RegisterParams {

View File

@@ -1,3 +1,2 @@
pub mod admin_seeder; pub mod admin_seeder;
pub mod shipping_seeder;
pub mod view_engine; pub mod view_engine;

View File

@@ -1,48 +0,0 @@
use async_trait::async_trait;
use loco_rs::prelude::*;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use crate::models::_entities::shipping_methods;
/// (code, display name, price in cents, requires a pickup point)
const CARRIERS: [(&str, &str, i64, bool); 3] = [
("packeta", "Packeta", 300, true),
("dpd", "DPD", 450, false),
("dhl", "DHL", 500, false),
];
pub struct ShippingSeeder;
#[async_trait]
impl Initializer for ShippingSeeder {
fn name(&self) -> String {
"shipping-seeder".to_string()
}
async fn before_run(&self, ctx: &AppContext) -> Result<()> {
for (position, (code, name, price_cents, requires_pickup_point)) in
CARRIERS.iter().enumerate()
{
let exists = shipping_methods::Entity::find()
.filter(shipping_methods::Column::Code.eq(*code))
.one(&ctx.db)
.await?
.is_some();
if exists {
continue;
}
shipping_methods::ActiveModel {
code: Set((*code).to_string()),
name: Set((*name).to_string()),
price_cents: Set(*price_cents),
requires_pickup_point: Set(*requires_pickup_point),
enabled: Set(true),
position: Set(position as i32),
..Default::default()
}
.insert(&ctx.db)
.await?;
}
Ok(())
}
}

View File

@@ -4,6 +4,7 @@ pub mod data;
pub mod initializers; pub mod initializers;
pub mod mailers; pub mod mailers;
pub mod models; pub mod models;
pub mod seed;
pub mod shared; pub mod shared;
pub mod tasks; pub mod tasks;
pub mod views; pub mod views;

178
src/seed.rs Normal file
View File

@@ -0,0 +1,178 @@
//! Catalog seed data — run via `cargo loco seed`.
use loco_rs::prelude::*;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use crate::{
models::_entities::{categories, products},
shared::slug::slugify,
};
// -- Categories -----------------------------------------------------------
struct CategorySeed {
name: &'static str,
description: &'static str,
position: i32,
}
const CATEGORIES: &[CategorySeed] = &[
CategorySeed {
name: "Electronics",
description: "Audio, computing, and smart devices",
position: 0,
},
CategorySeed {
name: "Accessories",
description: "Cables, adapters, cases, and everyday essentials",
position: 1,
},
CategorySeed {
name: "Home & Office",
description: "Ergonomic furniture, lighting, and desk organization",
position: 2,
},
];
// -- Products -------------------------------------------------------------
struct ProductSeed {
name: &'static str,
description: &'static str,
price_cents: i64,
stock: i32,
category_slug: &'static str,
sku: Option<&'static str>,
}
const PRODUCTS: &[ProductSeed] = &[
ProductSeed {
name: "Wireless Headphones",
description: "Over-ear Bluetooth headphones with active noise cancelling, 30-hour battery life, and plush memory-foam cushions.",
price_cents: 7_999,
stock: 25,
category_slug: "electronics",
sku: Some("WH-1000"),
},
ProductSeed {
name: "Mechanical Keyboard",
description: "Tenkeyless mechanical keyboard with hot-swappable switches, per-key RGB backlight, and a detachable USB-C cable.",
price_cents: 12_999,
stock: 15,
category_slug: "electronics",
sku: Some("MK-TKL-RGB"),
},
ProductSeed {
name: "USB-C Hub",
description: "7-in-1 USB-C hub with HDMI 4K output, 100 W power delivery pass-through, SD card reader, and three USB-A 3.2 ports.",
price_cents: 3_499,
stock: 40,
category_slug: "accessories",
sku: Some("USBC-HUB7"),
},
ProductSeed {
name: "Laptop Stand",
description: "Adjustable aluminium laptop stand with ventilated surface. Supports laptops from 10\u{201d} to 17\u{201d}.",
price_cents: 4_999,
stock: 30,
category_slug: "accessories",
sku: Some("LS-ALU-01"),
},
ProductSeed {
name: "Desk Lamp",
description: "LED desk lamp with 5 colour temperatures, stepless brightness control, and a flexible gooseneck arm.",
price_cents: 3_999,
stock: 20,
category_slug: "home-office",
sku: Some("DL-5CT"),
},
ProductSeed {
name: "Ergonomic Mouse",
description: "Vertical wireless ergonomic mouse with 6 buttons, adjustable DPI up to 4 000, and a sculpted thumb rest.",
price_cents: 5_999,
stock: 18,
category_slug: "electronics",
sku: Some("EM-VW-01"),
},
ProductSeed {
name: "Webcam Privacy Cover",
description: "Ultra-thin sliding webcam cover compatible with laptops, tablets, and external monitors. Pack of 3.",
price_cents: 599,
stock: 100,
category_slug: "accessories",
sku: Some("WPC-3PK"),
},
ProductSeed {
name: "Cable Organizer Set",
description: "Silicone cable management kit with 6 magnetic clips, 4 velcro straps, and an under-desk cable tray.",
price_cents: 1_299,
stock: 50,
category_slug: "home-office",
sku: Some("COS-MAG"),
},
];
// -- Public API -----------------------------------------------------------
/// Insert starter categories and products. Called from the `seed()` hook.
pub async fn seed_catalog(ctx: &AppContext) -> Result<()> {
for cat in CATEGORIES {
let slug = slugify(cat.name);
let exists = categories::Entity::find()
.filter(categories::Column::Slug.eq(&slug))
.one(&ctx.db)
.await?
.is_some();
if exists {
continue;
}
categories::ActiveModel {
name: Set(cat.name.to_string()),
slug: Set(slug),
description: Set(Some(cat.description.to_string())),
position: Set(cat.position),
published: Set(true),
..Default::default()
}
.insert(&ctx.db)
.await?;
}
for item in PRODUCTS {
let product_slug = slugify(item.name);
let exists = products::Entity::find()
.filter(products::Column::Slug.eq(&product_slug))
.one(&ctx.db)
.await?
.is_some();
if exists {
continue;
}
let cat_slug = slugify(item.category_slug);
let category = categories::Entity::find()
.filter(categories::Column::Slug.eq(&cat_slug))
.one(&ctx.db)
.await?;
let now = chrono::Utc::now();
products::ActiveModel {
name: Set(item.name.to_string()),
slug: Set(product_slug),
description: Set(Some(item.description.to_string())),
price_cents: Set(item.price_cents),
currency: Set("EUR".to_string()),
sku: Set(item.sku.map(|s| s.to_string())),
stock: Set(item.stock),
published: Set(true),
published_at: Set(Some(now.into())),
category_id: Set(category.map(|c| c.id)),
..Default::default()
}
.insert(&ctx.db)
.await?;
}
Ok(())
}