feat: add separate jobs and leads services
- Create jobs service (port 9103) for job postings management - Create leads service (port 9118) for lead/requirement management - Update gateway to route /api/jobs to jobs service - Update gateway to route /api/leads to leads service - Add jobs and leads to Woodpecker CI matrix
This commit is contained in:
parent
81c00eca96
commit
1e6abd9397
8 changed files with 364 additions and 3 deletions
|
|
@ -7,6 +7,8 @@ matrix:
|
||||||
- gateway
|
- gateway
|
||||||
- users
|
- users
|
||||||
- companies
|
- companies
|
||||||
|
- jobs
|
||||||
|
- leads
|
||||||
- job-seekers
|
- job-seekers
|
||||||
- customers
|
- customers
|
||||||
- payments
|
- payments
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ members = [
|
||||||
"apps/cron",
|
"apps/cron",
|
||||||
"apps/employees",
|
"apps/employees",
|
||||||
"apps/payments",
|
"apps/payments",
|
||||||
|
"apps/jobs",
|
||||||
|
"apps/leads",
|
||||||
"crates/db-migrate"
|
"crates/db-migrate"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -51,3 +53,4 @@ lettre = { version = "0.11", default-features = false, features = ["tokio1-rustl
|
||||||
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
|
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
tower-http = "0.6"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
struct Services {
|
struct Services {
|
||||||
users_url: String,
|
users_url: String,
|
||||||
companies_url: String,
|
companies_url: String,
|
||||||
|
jobs_url: String,
|
||||||
|
leads_url: String,
|
||||||
job_seekers_url: String,
|
job_seekers_url: String,
|
||||||
customers_url: String,
|
customers_url: String,
|
||||||
// ── 9 separate profession services ────────────────────────────────────
|
// ── 9 separate profession services ────────────────────────────────────
|
||||||
|
|
@ -41,6 +43,10 @@ impl Services {
|
||||||
.unwrap_or_else(|_| "http://localhost:9101".to_string()),
|
.unwrap_or_else(|_| "http://localhost:9101".to_string()),
|
||||||
companies_url: std::env::var("COMPANIES_SERVICE_URL")
|
companies_url: std::env::var("COMPANIES_SERVICE_URL")
|
||||||
.unwrap_or_else(|_| "http://localhost:9102".to_string()),
|
.unwrap_or_else(|_| "http://localhost:9102".to_string()),
|
||||||
|
jobs_url: std::env::var("JOBS_SERVICE_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:9103".to_string()),
|
||||||
|
leads_url: std::env::var("LEADS_SERVICE_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:9118".to_string()),
|
||||||
job_seekers_url: std::env::var("JOB_SEEKERS_SERVICE_URL")
|
job_seekers_url: std::env::var("JOB_SEEKERS_SERVICE_URL")
|
||||||
.unwrap_or_else(|_| "http://localhost:9104".to_string()),
|
.unwrap_or_else(|_| "http://localhost:9104".to_string()),
|
||||||
customers_url: std::env::var("CUSTOMERS_SERVICE_URL")
|
customers_url: std::env::var("CUSTOMERS_SERVICE_URL")
|
||||||
|
|
@ -115,17 +121,27 @@ impl Services {
|
||||||
{
|
{
|
||||||
Some(self.employees_url.clone())
|
Some(self.employees_url.clone())
|
||||||
}
|
}
|
||||||
// Companies + Jobs + Applications + Packages
|
// Companies + Applications + Packages
|
||||||
else if path.starts_with("/api/companies")
|
else if path.starts_with("/api/companies")
|
||||||
|| path.starts_with("/api/jobs")
|
|
||||||
|| path.starts_with("/api/applications")
|
|| path.starts_with("/api/applications")
|
||||||
|| path.starts_with("/api/pricing")
|
|| path.starts_with("/api/pricing")
|
||||||
|| path.starts_with("/api/admin/companies")
|
|| path.starts_with("/api/admin/companies")
|
||||||
|| path.starts_with("/api/admin/jobs")
|
|
||||||
|| path.starts_with("/api/admin/applications")
|
|| path.starts_with("/api/admin/applications")
|
||||||
{
|
{
|
||||||
Some(self.companies_url.clone())
|
Some(self.companies_url.clone())
|
||||||
}
|
}
|
||||||
|
// Jobs (separate service)
|
||||||
|
else if path.starts_with("/api/jobs")
|
||||||
|
|| path.starts_with("/api/admin/jobs")
|
||||||
|
{
|
||||||
|
Some(self.jobs_url.clone())
|
||||||
|
}
|
||||||
|
// Leads (separate service)
|
||||||
|
else if path.starts_with("/api/leads")
|
||||||
|
|| path.starts_with("/api/admin/leads")
|
||||||
|
{
|
||||||
|
Some(self.leads_url.clone())
|
||||||
|
}
|
||||||
// Job Seekers
|
// Job Seekers
|
||||||
else if path.starts_with("/api/jobseeker") {
|
else if path.starts_with("/api/jobseeker") {
|
||||||
Some(self.job_seekers_url.clone())
|
Some(self.job_seekers_url.clone())
|
||||||
|
|
|
||||||
21
apps/jobs/Cargo.toml
Normal file
21
apps/jobs/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "jobs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sqlx = { workspace = true }
|
||||||
|
axum = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "jobs"
|
||||||
|
path = "src/main.rs"
|
||||||
136
apps/jobs/src/main.rs
Normal file
136
apps/jobs/src/main.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
routing::{get, post, put, delete},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Job {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub location: String,
|
||||||
|
pub job_type: String,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreateJob {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub location: String,
|
||||||
|
pub job_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_jobs(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Job>>, StatusCode> {
|
||||||
|
let jobs = sqlx::query_as::<_, Job>(
|
||||||
|
"SELECT id, title, description, location, job_type, status, created_at FROM jobs ORDER BY created_at DESC"
|
||||||
|
)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
Ok(Json(jobs))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_job(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<CreateJob>,
|
||||||
|
) -> Result<Json<Job>, StatusCode> {
|
||||||
|
let job = sqlx::query_as::<_, Job>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO jobs (title, description, location, job_type)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, title, description, location, job_type, status, created_at
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&payload.title)
|
||||||
|
.bind(&payload.description)
|
||||||
|
.bind(&payload.location)
|
||||||
|
.bind(&payload.job_type)
|
||||||
|
.fetch_one(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
Ok(Json(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_job(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||||
|
) -> Result<Json<Job>, StatusCode> {
|
||||||
|
let job = sqlx::query_as::<_, Job>(
|
||||||
|
"SELECT id, title, description, location, job_type, status, created_at FROM jobs WHERE id = $1"
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
Ok(Json(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health() -> &'static str {
|
||||||
|
"Jobs Service OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::EnvFilter::new(
|
||||||
|
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
|
||||||
|
))
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let database_url = std::env::var("DATABASE_URL")
|
||||||
|
.expect("DATABASE_URL must be set");
|
||||||
|
|
||||||
|
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||||
|
.max_connections(10)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to database");
|
||||||
|
|
||||||
|
tracing::info!("Connected to database");
|
||||||
|
|
||||||
|
let state = Arc::new(AppState { pool });
|
||||||
|
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_origin(Any)
|
||||||
|
.allow_methods(Any)
|
||||||
|
.allow_headers(Any);
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/health", get(health))
|
||||||
|
.route("/jobs", get(list_jobs))
|
||||||
|
.route("/jobs", post(create_job))
|
||||||
|
.route("/jobs/:id", get(get_job))
|
||||||
|
.layer(cors)
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let port: u16 = std::env::var("PORT")
|
||||||
|
.unwrap_or_else(|_| "9103".to_string())
|
||||||
|
.parse()
|
||||||
|
.expect("PORT must be a valid u16");
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
|
||||||
|
tracing::info!("Jobs service listening on {}", addr);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
||||||
21
apps/leads/Cargo.toml
Normal file
21
apps/leads/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "leads"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sqlx = { workspace = true }
|
||||||
|
axum = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "leads"
|
||||||
|
path = "src/main.rs"
|
||||||
136
apps/leads/src/main.rs
Normal file
136
apps/leads/src/main.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
routing::{get, post, put, delete},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Lead {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub location: String,
|
||||||
|
pub profession_key: String,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreateLead {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub location: String,
|
||||||
|
pub profession_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_leads(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Lead>>, StatusCode> {
|
||||||
|
let leads = sqlx::query_as::<_, Lead>(
|
||||||
|
"SELECT id, title, description, location, profession_key, status, created_at FROM requirements ORDER BY created_at DESC"
|
||||||
|
)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
Ok(Json(leads))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_lead(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<CreateLead>,
|
||||||
|
) -> Result<Json<Lead>, StatusCode> {
|
||||||
|
let lead = sqlx::query_as::<_, Lead>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO requirements (title, description, location, profession_key)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, title, description, location, profession_key, status, created_at
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&payload.title)
|
||||||
|
.bind(&payload.description)
|
||||||
|
.bind(&payload.location)
|
||||||
|
.bind(&payload.profession_key)
|
||||||
|
.fetch_one(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
Ok(Json(lead))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_lead(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||||
|
) -> Result<Json<Lead>, StatusCode> {
|
||||||
|
let lead = sqlx::query_as::<_, Lead>(
|
||||||
|
"SELECT id, title, description, location, profession_key, status, created_at FROM requirements WHERE id = $1"
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
Ok(Json(lead))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health() -> &'static str {
|
||||||
|
"Leads Service OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::EnvFilter::new(
|
||||||
|
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
|
||||||
|
))
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let database_url = std::env::var("DATABASE_URL")
|
||||||
|
.expect("DATABASE_URL must be set");
|
||||||
|
|
||||||
|
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||||
|
.max_connections(10)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to database");
|
||||||
|
|
||||||
|
tracing::info!("Connected to database");
|
||||||
|
|
||||||
|
let state = Arc::new(AppState { pool });
|
||||||
|
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_origin(Any)
|
||||||
|
.allow_methods(Any)
|
||||||
|
.allow_headers(Any);
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/health", get(health))
|
||||||
|
.route("/leads", get(list_leads))
|
||||||
|
.route("/leads", post(create_lead))
|
||||||
|
.route("/leads/:id", get(get_lead))
|
||||||
|
.layer(cors)
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let port: u16 = std::env::var("PORT")
|
||||||
|
.unwrap_or_else(|_| "9118".to_string())
|
||||||
|
.parse()
|
||||||
|
.expect("PORT must be a valid u16");
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
|
||||||
|
tracing::info!("Leads service listening on {}", addr);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,8 @@ services:
|
||||||
ADMIN_URL: http://localhost:9202
|
ADMIN_URL: http://localhost:9202
|
||||||
USERS_SERVICE_URL: http://users:9101
|
USERS_SERVICE_URL: http://users:9101
|
||||||
COMPANIES_SERVICE_URL: http://companies:9102
|
COMPANIES_SERVICE_URL: http://companies:9102
|
||||||
|
JOBS_SERVICE_URL: http://jobs:9103
|
||||||
|
LEADS_SERVICE_URL: http://leads:9118
|
||||||
JOB_SEEKERS_SERVICE_URL: http://job-seekers:9104
|
JOB_SEEKERS_SERVICE_URL: http://job-seekers:9104
|
||||||
CUSTOMERS_SERVICE_URL: http://customers:9105
|
CUSTOMERS_SERVICE_URL: http://customers:9105
|
||||||
EMPLOYEES_SERVICE_URL: http://employees:9106
|
EMPLOYEES_SERVICE_URL: http://employees:9106
|
||||||
|
|
@ -71,6 +73,10 @@ services:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
companies:
|
companies:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
|
jobs:
|
||||||
|
condition: service_started
|
||||||
|
leads:
|
||||||
|
condition: service_started
|
||||||
job-seekers:
|
job-seekers:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
customers:
|
customers:
|
||||||
|
|
@ -130,6 +136,26 @@ services:
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
platform: linux/amd64
|
||||||
|
image: ghcr.io/traceworks2023/nxtgauge-rust-jobs:high-performance-latest
|
||||||
|
environment:
|
||||||
|
PORT: "9103"
|
||||||
|
DATABASE_URL: postgresql://nxtgauge:nxtgauge_dev@postgres:5432/nxtgauge_db
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
leads:
|
||||||
|
platform: linux/amd64
|
||||||
|
image: ghcr.io/traceworks2023/nxtgauge-rust-leads:high-performance-latest
|
||||||
|
environment:
|
||||||
|
PORT: "9118"
|
||||||
|
DATABASE_URL: postgresql://nxtgauge:nxtgauge_dev@postgres:5432/nxtgauge_db
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
job-seekers:
|
job-seekers:
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
image: ghcr.io/traceworks2023/nxtgauge-rust-job-seekers:high-performance-latest
|
image: ghcr.io/traceworks2023/nxtgauge-rust-job-seekers:high-performance-latest
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue