Task 03 — Industries with Their Users
Difficulty: Beginner–Intermediate
Goal: Add an admin endpoint that returns all industries, each with the list of users belonging to that industry, merged into a single response.
What you'll learn
- How to make multiple parallel DB calls from a single service
- How to join/merge two datasets in the service layer
- Using
ThreadPoolExecutorfor concurrent queries
Endpoint spec
GET /api/admin/industries/with-users
Authorization: Bearer <admin-token>
Response 200:
{
"status": "success",
"status_code": 200,
"data": [
{
"industryId": "I001",
"industryName": "Industry A",
"users": [
{ "userId": "I001_1234_alice", "username": "alice", "type": "DEFAULT", "active": true },
{ "userId": "I001_5678_bob", "username": "bob", "type": "OTP", "active": true }
]
}
]
}
Real field names from the backend
- Industry documents use
industryId(format:I1,I2,I100) andindustryName - User documents use
userId,username,type(DEFAULT/OTP/SSO/microsoft),active,email,phoneNo - There is no
namefield on users — the display name field isusername
Files to create / edit
| File | What to do |
|---|---|
app/routes/admin/industry_users.py | Define namespace + resource |
app/services/admin/industry_users_service.py | Fetch industries + users, merge |
app/formatters/admin/industry_users_formatter.py | Shape the merged output |
app/apis/admin.py | Register the namespace |
app/database/queires_list.py | No new queries needed — use existing methods |
app/database/database_supporter.py | No new methods needed — use get_workspace_data() and get_all_users_of_industry() |
Service — key logic
Use ThreadPoolExecutor to fetch users for all industries concurrently instead of making N sequential calls.
The real DB method to get all users for an industry is DatabaseSupporter.get_all_users_of_industry(industry_id) — it returns id, username, type, active fields.
To get workspace data (all industries), use DatabaseSupporter.get_workspace_data() — it already exists and returns industryId, industryName, meta.logo, meta.sector, meta.subSector.
# app/services/admin/industry_users_service.py
from concurrent.futures import ThreadPoolExecutor
from app.database.database_supporter import DatabaseSupporter
class IndustryUsersService:
def get_all(self):
# get_workspace_data() already exists — returns all industries with key fields
industries = DatabaseSupporter.get_workspace_data()
def fetch_users(industry):
# get_all_users_of_industry() already exists — returns id, username, type, active
users = DatabaseSupporter.get_all_users_of_industry(industry['industryId'])
return {**industry, 'users': users}
with ThreadPoolExecutor() as executor:
results = list(executor.map(fetch_users, industries))
return results
Formatter
class IndustryUsersFormatter:
def __init__(self, data):
self.data = data
def format(self):
return [
{
'industryId': ind['industryId'],
'industryName': ind.get('industryName'), # real field is 'industryName', not 'name'
'users': [
{
'userId': u.get('id'), # get_all_users_of_industry returns 'id'
'username': u.get('username'),
'type': u.get('type'), # 'DEFAULT', 'OTP', 'SSO', 'microsoft'
'active': u.get('active'),
}
for u in ind.get('users', [])
]
}
for ind in self.data
]
Things to watch out for
- Industries with zero users should still appear in the response, with
"users": []. - Keep the user query scoped to one industry at a time — never
SELECT * FROM cacross the full users container.
Done when
- Response contains all industries
- Each industry object has a
usersarray (empty array is fine if no users) - Response time stays reasonable even with many industries (parallel fetching works)