Initial commit: Full Crawl API implementation
Some checks failed
CI / Test (push) Has been cancelled
Deploy / Deploy to Staging (push) Has been cancelled
CI / Build & Push (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled

This commit is contained in:
2026-04-29 07:03:48 +00:00
commit 62994d4f3d
92 changed files with 6176 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
use sqlx::PgPool;
pub type DbPool = PgPool;
pub async fn create_pool(database_url: &str) -> Result<DbPool, sqlx::Error> {
PgPool::connect(database_url).await
}

4
crates/db/src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod connection;
pub mod repos;
pub use connection::DbPool;

View File

@@ -0,0 +1,64 @@
use shared::models::ApiKey;
use sqlx::PgPool;
use uuid::Uuid;
pub async fn find_by_key_hash(pool: &PgPool, key_hash: &str) -> Result<Option<ApiKey>, sqlx::Error> {
sqlx::query_as::<_, ApiKey>(
r#"SELECT id, user_id, key_hash, name, last_used_at, created_at FROM api_keys WHERE key_hash = $1"#,
)
.bind(key_hash)
.fetch_optional(pool)
.await
}
pub async fn create(
pool: &PgPool,
user_id: Uuid,
key_hash: &str,
name: &str,
) -> Result<ApiKey, sqlx::Error> {
sqlx::query_as::<_, ApiKey>(
r#"INSERT INTO api_keys (id, user_id, key_hash, name)
VALUES ($1, $2, $3, $4)
RETURNING id, user_id, key_hash, name, last_used_at, created_at"#,
)
.bind(Uuid::new_v4())
.bind(user_id)
.bind(key_hash)
.bind(name)
.fetch_one(pool)
.await
}
pub async fn list_by_user(pool: &PgPool, user_id: Uuid) -> Result<Vec<ApiKey>, sqlx::Error> {
sqlx::query_as::<_, ApiKey>(
r#"SELECT id, user_id, key_hash, name, last_used_at, created_at FROM api_keys WHERE user_id = $1 ORDER BY created_at DESC"#,
)
.bind(user_id)
.fetch_all(pool)
.await
}
pub async fn update_last_used(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE api_keys SET last_used_at = $1 WHERE id = $2"#,
)
.bind(chrono::Utc::now())
.bind(id)
.execute(pool)
.await?;
Ok(())
}
pub async fn delete_by_id(pool: &PgPool, id: Uuid, user_id: Uuid) -> Result<bool, sqlx::Error> {
let result = sqlx::query(
r#"DELETE FROM api_keys WHERE id = $1 AND user_id = $2"#,
)
.bind(id)
.bind(user_id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}

View File

@@ -0,0 +1,6 @@
pub mod api_keys;
pub mod oauth;
pub mod subscriptions;
pub mod teams;
pub mod usage_logs;
pub mod users;

View File

@@ -0,0 +1,37 @@
use shared::models::OAuthAccount;
use sqlx::PgPool;
use uuid::Uuid;
pub async fn find_by_provider(
pool: &PgPool,
provider: &str,
provider_account_id: &str,
) -> Result<Option<OAuthAccount>, sqlx::Error> {
sqlx::query_as::<_, OAuthAccount>(
r#"SELECT id, user_id, provider, provider_account_id, created_at
FROM oauth_accounts WHERE provider = $1 AND provider_account_id = $2"#,
)
.bind(provider)
.bind(provider_account_id)
.fetch_optional(pool)
.await
}
pub async fn create(
pool: &PgPool,
user_id: Uuid,
provider: &str,
provider_account_id: &str,
) -> Result<OAuthAccount, sqlx::Error> {
sqlx::query_as::<_, OAuthAccount>(
r#"INSERT INTO oauth_accounts (id, user_id, provider, provider_account_id)
VALUES ($1, $2, $3, $4)
RETURNING id, user_id, provider, provider_account_id, created_at"#,
)
.bind(Uuid::new_v4())
.bind(user_id)
.bind(provider)
.bind(provider_account_id)
.fetch_one(pool)
.await
}

View File

@@ -0,0 +1,76 @@
use shared::models::Subscription;
use sqlx::PgPool;
use uuid::Uuid;
pub async fn find_by_user(pool: &PgPool, user_id: Uuid) -> Result<Option<Subscription>, sqlx::Error> {
sqlx::query_as::<_, Subscription>(
r#"SELECT id, user_id, stripe_customer_id, stripe_subscription_id, stripe_price_id,
status, tier, current_period_start, current_period_end, created_at, updated_at
FROM subscriptions WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn find_by_stripe_subscription(
pool: &PgPool,
stripe_subscription_id: &str,
) -> Result<Option<Subscription>, sqlx::Error> {
sqlx::query_as::<_, Subscription>(
r#"SELECT id, user_id, stripe_customer_id, stripe_subscription_id, stripe_price_id,
status, tier, current_period_start, current_period_end, created_at, updated_at
FROM subscriptions WHERE stripe_subscription_id = $1"#,
)
.bind(stripe_subscription_id)
.fetch_optional(pool)
.await
}
pub async fn create_or_update(
pool: &PgPool,
user_id: Uuid,
stripe_customer_id: Option<&str>,
stripe_subscription_id: Option<&str>,
stripe_price_id: Option<&str>,
status: &str,
tier: &str,
) -> Result<Subscription, sqlx::Error> {
sqlx::query_as::<_, Subscription>(
r#"INSERT INTO subscriptions (id, user_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, status, tier)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (user_id) DO UPDATE SET
stripe_customer_id = EXCLUDED.stripe_customer_id,
stripe_subscription_id = EXCLUDED.stripe_subscription_id,
stripe_price_id = EXCLUDED.stripe_price_id,
status = EXCLUDED.status,
tier = EXCLUDED.tier,
updated_at = NOW()
RETURNING id, user_id, stripe_customer_id, stripe_subscription_id, stripe_price_id,
status, tier, current_period_start, current_period_end, created_at, updated_at"#,
)
.bind(Uuid::new_v4())
.bind(user_id)
.bind(stripe_customer_id)
.bind(stripe_subscription_id)
.bind(stripe_price_id)
.bind(status)
.bind(tier)
.fetch_one(pool)
.await
}
pub async fn update_status(
pool: &PgPool,
stripe_subscription_id: &str,
status: &str,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE subscriptions SET status = $1, updated_at = NOW() WHERE stripe_subscription_id = $2"#,
)
.bind(status)
.bind(stripe_subscription_id)
.execute(pool)
.await?;
Ok(())
}

View File

@@ -0,0 +1,68 @@
use shared::models::{Team, TeamMember};
use sqlx::PgPool;
use uuid::Uuid;
pub async fn create(pool: &PgPool, name: &str, slug: &str, owner_id: Uuid) -> Result<Team, sqlx::Error> {
sqlx::query_as::<_, Team>(
r#"INSERT INTO teams (id, name, slug, owner_id)
VALUES ($1, $2, $3, $4)
RETURNING id, name, slug, owner_id, created_at, updated_at"#,
)
.bind(Uuid::new_v4())
.bind(name)
.bind(slug)
.bind(owner_id)
.fetch_one(pool)
.await
}
pub async fn find_by_slug(pool: &PgPool, slug: &str) -> Result<Option<Team>, sqlx::Error> {
sqlx::query_as::<_, Team>(
r#"SELECT id, name, slug, owner_id, created_at, updated_at FROM teams WHERE slug = $1"#,
)
.bind(slug)
.fetch_optional(pool)
.await
}
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Team>, sqlx::Error> {
sqlx::query_as::<_, Team>(
r#"SELECT id, name, slug, owner_id, created_at, updated_at FROM teams WHERE id = $1"#,
)
.bind(id)
.fetch_optional(pool)
.await
}
pub async fn add_member(pool: &PgPool, team_id: Uuid, user_id: Uuid, role: &str) -> Result<TeamMember, sqlx::Error> {
sqlx::query_as::<_, TeamMember>(
r#"INSERT INTO team_members (id, team_id, user_id, role)
VALUES ($1, $2, $3, $4)
RETURNING id, team_id, user_id, role, created_at"#,
)
.bind(Uuid::new_v4())
.bind(team_id)
.bind(user_id)
.bind(role)
.fetch_one(pool)
.await
}
pub async fn list_members(pool: &PgPool, team_id: Uuid) -> Result<Vec<TeamMember>, sqlx::Error> {
sqlx::query_as::<_, TeamMember>(
r#"SELECT id, team_id, user_id, role, created_at FROM team_members WHERE team_id = $1"#,
)
.bind(team_id)
.fetch_all(pool)
.await
}
pub async fn find_member(pool: &PgPool, team_id: Uuid, user_id: Uuid) -> Result<Option<TeamMember>, sqlx::Error> {
sqlx::query_as::<_, TeamMember>(
r#"SELECT id, team_id, user_id, role, created_at FROM team_members WHERE team_id = $1 AND user_id = $2"#,
)
.bind(team_id)
.bind(user_id)
.fetch_optional(pool)
.await
}

View File

@@ -0,0 +1,47 @@
use shared::models::UsageLog;
use sqlx::PgPool;
use uuid::Uuid;
pub async fn create(
pool: &PgPool,
user_id: Uuid,
api_key_id: Uuid,
endpoint: &str,
url: &str,
status: &str,
credits_used: i64,
duration_ms: i64,
) -> Result<UsageLog, sqlx::Error> {
sqlx::query_as::<_, UsageLog>(
r#"INSERT INTO usage_logs (id, user_id, api_key_id, endpoint, url, status, credits_used, duration_ms)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, user_id, api_key_id, endpoint, url, status, credits_used, duration_ms, created_at"#,
)
.bind(Uuid::new_v4())
.bind(user_id)
.bind(api_key_id)
.bind(endpoint)
.bind(url)
.bind(status)
.bind(credits_used)
.bind(duration_ms)
.fetch_one(pool)
.await
}
pub async fn list_by_user(
pool: &PgPool,
user_id: Uuid,
limit: i64,
offset: i64,
) -> Result<Vec<UsageLog>, sqlx::Error> {
sqlx::query_as::<_, UsageLog>(
r#"SELECT id, user_id, api_key_id, endpoint, url, status, credits_used, duration_ms, created_at
FROM usage_logs WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3"#,
)
.bind(user_id)
.bind(limit)
.bind(offset)
.fetch_all(pool)
.await
}

View File

@@ -0,0 +1,64 @@
use shared::models::User;
use sqlx::PgPool;
use uuid::Uuid;
pub async fn find_by_email(pool: &PgPool, email: &str) -> Result<Option<User>, sqlx::Error> {
sqlx::query_as::<_, User>(
r#"SELECT id, email, password_hash, google_id, credits, tier, created_at, updated_at FROM users WHERE email = $1"#,
)
.bind(email)
.fetch_optional(pool)
.await
}
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<User>, sqlx::Error> {
sqlx::query_as::<_, User>(
r#"SELECT id, email, password_hash, google_id, credits, tier, created_at, updated_at FROM users WHERE id = $1"#,
)
.bind(id)
.fetch_optional(pool)
.await
}
pub async fn create(
pool: &PgPool,
email: &str,
password_hash: Option<&str>,
google_id: Option<&str>,
) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>(
r#"INSERT INTO users (id, email, password_hash, google_id, credits, tier)
VALUES ($1, $2, $3, $4, 30, 'free')
RETURNING id, email, password_hash, google_id, credits, tier, created_at, updated_at"#,
)
.bind(Uuid::new_v4())
.bind(email)
.bind(password_hash)
.bind(google_id)
.fetch_one(pool)
.await
}
pub async fn deduct_credits(pool: &PgPool, user_id: Uuid, amount: i64) -> Result<bool, sqlx::Error> {
let result = sqlx::query(
r#"UPDATE users SET credits = credits - $1 WHERE id = $2 AND credits >= $1"#,
)
.bind(amount)
.bind(user_id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}
pub async fn add_credits(pool: &PgPool, user_id: Uuid, amount: i64) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE users SET credits = credits + $1 WHERE id = $2"#,
)
.bind(amount)
.bind(user_id)
.execute(pool)
.await?;
Ok(())
}