initial seed
This commit is contained in:
@@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
pub mod admin_seeder;
|
pub mod admin_seeder;
|
||||||
pub mod shipping_seeder;
|
|
||||||
pub mod view_engine;
|
pub mod view_engine;
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
178
src/seed.rs
Normal 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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user