From 95f195a20459affff63475550519ad44532b967a Mon Sep 17 00:00:00 2001 From: Priec Date: Wed, 17 Jun 2026 13:40:21 +0200 Subject: [PATCH] initial seed --- src/app.rs | 2 +- src/initializers/admin_seeder.rs | 16 ++- src/initializers/mod.rs | 1 - src/initializers/shipping_seeder.rs | 48 -------- src/lib.rs | 1 + src/seed.rs | 178 ++++++++++++++++++++++++++++ 6 files changed, 195 insertions(+), 51 deletions(-) delete mode 100644 src/initializers/shipping_seeder.rs create mode 100644 src/seed.rs diff --git a/src/app.rs b/src/app.rs index fd84398..7ff1583 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,7 +60,6 @@ impl Hooks for App { Ok(vec![ Box::new(initializers::view_engine::ViewEngineInitializer), 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<()> { db::seed::(&ctx.db, &base.join("users.yaml").display().to_string()) .await?; + crate::seed::seed_catalog(ctx).await?; Ok(()) } } diff --git a/src/initializers/admin_seeder.rs b/src/initializers/admin_seeder.rs index b4d4620..daf0050 100644 --- a/src/initializers/admin_seeder.rs +++ b/src/initializers/admin_seeder.rs @@ -1,5 +1,7 @@ use async_trait::async_trait; use loco_rs::prelude::*; +use loco_rs::hash; +use sea_orm::{ActiveModelTrait, IntoActiveModel, Set}; use crate::models::users::{self, RegisterParams}; @@ -18,7 +20,19 @@ impl Initializer for AdminSeeder { if email.is_empty() || password.is_empty() { 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( &ctx.db, &RegisterParams { diff --git a/src/initializers/mod.rs b/src/initializers/mod.rs index 0f2a40c..a5780a7 100644 --- a/src/initializers/mod.rs +++ b/src/initializers/mod.rs @@ -1,3 +1,2 @@ pub mod admin_seeder; -pub mod shipping_seeder; pub mod view_engine; diff --git a/src/initializers/shipping_seeder.rs b/src/initializers/shipping_seeder.rs deleted file mode 100644 index 83ff8a1..0000000 --- a/src/initializers/shipping_seeder.rs +++ /dev/null @@ -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(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 1ab61e9..c96ed98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod data; pub mod initializers; pub mod mailers; pub mod models; +pub mod seed; pub mod shared; pub mod tasks; pub mod views; diff --git a/src/seed.rs b/src/seed.rs new file mode 100644 index 0000000..3c0458a --- /dev/null +++ b/src/seed.rs @@ -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(()) +}