diff --git a/Questions.txt b/Questions.txt new file mode 100644 index 0000000..36f5b8e --- /dev/null +++ b/Questions.txt @@ -0,0 +1,36 @@ +Q1: When a user changes the company. What happens to the project, resources and tasks? +I assume each company has its own set of project, resources and tasks and doesn't overlap +with other companies (p, r, t) ? + +i.e a project, resource and task belong to a single company. Is this understanding correct? + +Clarification: Projects have a many to one relationship with a Company and Tasks have a many to one relationship with a Project. +i.e. Assume that a project is not shared between companies and a task is not shared between projects. + +Q2: I estimate we don't need the scale of google and facebook reaching 1B users for our database +design, based on some estimate and fair idea about which business ditio is in lets estimate the +range for the number of users be in range of 100k-10M as of now. So as to scope our discussion. +What do you think of this estimate? + +Clarification: Yes at the moment, that scale is even smaller. But to design a solution for the range that you specified is more than enough. + +Q3: There can be many projects inside the company, so the user is shown only a subset of it - +project he is been assigned/scheduled? So a company might have project p1, p2, p3, p4 etc. +But user A is scheduled for project p1, p3. + +Is this understanding correct? + +Clarification: Yes that is correct. A user will usually work on a subset of projects in a company. + +Q4. Similarly, a project can have many tasks, so the user is shown only a subset of these tasks +or all the tasks? + +Clarification: Assume that the user has the ability to pick any of his scheduled projects and any task in the project when he checks in. + +Q5. I think resources are company wide entity and can be used across different projects and tasks +albeit only when free and not used in some other task. Is this understanding correct? +So the same resource R1 can be shown to user in Project P1 -> task T1 and Project P2 -> task T2? +If a user select a resource, then should it become unavailble to other users who might have +that resource scheduled for them? + +Clarification: Exactly right. diff --git a/SchemaAndQueries.txt b/SchemaAndQueries.txt new file mode 100644 index 0000000..8368af8 --- /dev/null +++ b/SchemaAndQueries.txt @@ -0,0 +1,104 @@ +# LIST OF ENTITIES AND RELATIONSHIPS + +1. users(user_id, user_name, email, password) + +2. projects(project_id, project_name, description, location) + +3. tasks(task_id, task_name, description, project_id) + +4. resources(resource_id, resource_name) + +5. companies(company_id, comapany_name) + +6. admins(admin_id, admin_name) + +7. user_works_for_company(user_id, company_id) + +8. user_scheduled_for_project(user_id, project_id) + +9. user_scheduled_for_resources(user_id, resource_id) + +10. user_tasks(user_task_id, user_id, task_id, resource_id, check_in_time, check_out_time) + + +# DB QUERIES FOR A TYPICAL TRANSACTION AT DITIO: + +Let's say we have a user in table users with following data, +(1234, "RAHUL", "RAHUL@GMAIL.COM", "HASH_PASSWORD") + +# User logins into the app. +1. SELECT * + FROM users + WHERE users.email == "RAHUL@GMAIL.COM" and password == "HASH_PASSWORD" + +# User chooses a company. +2. SELECT company_id, company_name + FROM companies + WHERE company_id IN (SELECT company_id + FROM user_works_for_company + WHERE user_id == 1234) + + + + +# If the assumption is correct and there is not an overlap between the project, task and resources +# from one company to another. We have a natural partition happening in the tables. + +# So for example, instead of having a single table project having project details of all the companies +# we can partition the table horizontally pulling out the data for each company into its separate tables +# for each company. + +# User chooses a company from the list of companies they work for, let say company_id = "company_123". +# Now we have sharded the database on company_id i.e we have a separate database running for every +# company having its own tables with from table numbers [2, 3, 4, 5, 6, 8, 9, 10] + +# we pick the database url to which the subsequent queries should hit, lets say we maintain a cache +# server with redis having (key, value) = (company_id, database_server_url) +# let say it looks like following, +# company_database_map = { + "ditioas" : "https://ditioas.mysql.database.azure.com", + "company_123" : "https://company_123.mysql.database.azure.com", + "vassabank" : "https://vassabank.mysql.database.azure.com", + } +# so the following queries will go to "https://company_123.mysql.database.azure.com". + +# User gets a list of project which are scheduled for them and selects one project. +3. SELECT project_name, description, location + FROM projects + WHERE project_id IN (SELECT project_id + FROM user_scheduled_for_project + WHERE user_id = 1234) + +# User gets a list of resources which are scheduled for them and selects one resource. +4. SELECT resource_id, resource_name + FROM resources + WHERE resource_id IN (SELECT resource_id + FROM user_scheduled_for_resources + WHERE user_id = 1234) + +# Let say user chooses a project with project_id = "project_123" and resource with resource_id = "resource_123" +# User get a list of tasks belonging to the chosen project and chooses one task. +5. SELECT tasks.task_id, tasks.task_name, tasks.description + FROM tasks + WHERE tasks.project_id = "project_123" + AND tasks.task_id IN ( + SELECT user_scheduled_for_task.task_id + FROM user_scheduled_for_task + WHERE user_scheduled_for_task.user_id = 1234 + ); + + +# Let say user chooses a task with task_id = "task_123" +# User clicks on check-in and records the check-in time. +6. UPDATE user_scheduled_for_task + SET resource_id = "resource_123", checkin_time = '2023-01-25 09:00:00' + WHERE user_id = 1234 AND task_id = "task_123" + +# User clicks on check-out and records the check-out time. +7. UPDATE user_scheduled_for_task + SET checkout_time = '2023-01-25 11:00:00' + WHERE user_id = 1234 AND task_id = "task_123" + + + + diff --git a/backend/ApiLayer.cs b/backend/ApiLayer.cs new file mode 100644 index 0000000..15d67ed --- /dev/null +++ b/backend/ApiLayer.cs @@ -0,0 +1,76 @@ +public class UserController : ControllerBase +{ + private readonly IUserService _userService; + + [HttpGet("{userId}/companies")] + public IActionResult GetAllCompaniesForUser(int userId) + { + var companies = _userService.GetAllCompaniesForUser(userId); + return Ok(companies); + } + + [HttpGet("{userId}/projects")] + public IActionResult GetAllProjectsForUser(int userId) + { + var projects = _userService.GetAllProjectsForUser(userId); + return Ok(projects); + } + + [HttpGet("{userId}/resources")] + public IActionResult GetAllAvailableResourcesForUser(int userId) + { + var resources = _userService.GetAllAvailableResourcesForUser(userId); + return Ok(resources); + } +} + +public class CompanyController : ControllerBase +{ + private readonly ICompanyService _companyService; + + [HttpGet("{companyId}/databaseUrl")] + public ActionResult GetDatabaseUrl(int companyId) + { + var url = _companyService.GetDatabaseUrl(companyId); + return Ok(url); + } +} + +public class ProjectController : ControllerBase +{ + private readonly IProjectService _projectService; + + [HttpGet("{projectId}/tasks")] + public ActionResult> GetAllTasksForProject(int projectId) + { + var tasks = _projectService.GetAllTasksForProject(projectId); + return Ok(tasks); + } +} + +public class UserTaskController : ControllerBase +{ + private readonly IUserTaskService _userTaskService; + private readonly IResourceService _resourceService; + + [HttpPost("checkin")] + public IActionResult Checkin(int userId, int taskId, int resourceId, DateTime checkInTime) + { + // on check in user creates a task, with default value for istaskcompleted as false + _userTaskService.CreateUserTask(userId, taskId, resourceId, checkInTime); + _resourceService.UpdateResourceAvailability(resourceId, resourceAvailable=false); + return Ok(); + } + + [HttpPost("checkout")] + public IActionResult Checkout(int userId, int taskId, int resourceId, DateTime checkOutTime) + { + // Get the usertaskid for user,task,resource combination where taskisnotcompleted + int userTaskId = userTaskService.GetUserTaskId(userId, taskId, resourceId, taskCompleted=false); + + // Update the task as completed, record the checkout time and set the resource available + userTaskService.UpdateUserTaskCompletion(userTaskId, checkOutTime, taskCompleted=true); + resourceService.UpdateResourceAvailability(resourceId, resourceAvailable=true); + return Ok(); + } +} diff --git a/backend/Models.cs b/backend/Models.cs new file mode 100644 index 0000000..3932eb5 --- /dev/null +++ b/backend/Models.cs @@ -0,0 +1,120 @@ +public class User +{ + public int UserId { get; set; } + + public string UserName { get; set; } + + public string UserEmail { get; set; } + + public string Password { get; set; } + + public List UserWorksForCompany {get; set;} + + public List ProjectScheduledForUser {get; set;} + + public List ResourceScheduledForUser {get; set;} +} + +public class Company +{ + public int CompanyId { get; set; } + + public string CompanyName { get; set; } + + public string CompanyDatabaseUrl { get; set; } + + public List UserWorksForCompany {get; set;} + + public List Project {get; set;} + + public List Resource {get; set;} +} + +public class UserWorksForCompany +{ + public int UserId { get; set; } + + public string CompanyId { get; set; } + + public User User { get; set; } + + public Company Company { get; set; } +} + +public class Project +{ + public int ProjectId { get; set; } + + public string ProjectName { get; set; } + + public string ProjectDescription { get; set; } + + public List ProjectScheduledForUser {get; set; } + + public Company Company {get; set;} + + public List Task; +} + +public class ProjectScheduledForUser +{ + public int ProjectId { get; set; } + + public int UserId {get; set;} + + public Project Project {get; set;} + + public User User {get; set;} +} + +public class Resource +{ + public int ResourceId { get; set; } + + public string ResourceName { get; set; } + + public bool ResourceAvailable { get; set; } + + public Company Company {get; set;} + + public List ResourceScheduledForUser; +} + +public class ResourceScheduledForUser +{ + public int ResourceId { get; set; } + + public int UserId {get; set;} + + public Resource Resource {get; set;} + + public User User {get; set;} +} + +public class Task +{ + public int TaskId { get; set; } + + public string TaskName { get; set; } + + public string TaskDescription { get; set; } + + public Project Project {get; set;} +} + +public class UserTask +{ + public int UserTaskId { get; set; } + + public int UserId { get; set; } + + public int TaskId { get; set; } + + public int ResourceId { get; set; } + + public bool TaskCompleted {get; set;} + + public DateTime CheckInTime { get; set; } + + public DateTime CheckOutTime { get; set; } +} diff --git a/backend/ServiceLayer.cs b/backend/ServiceLayer.cs new file mode 100644 index 0000000..e67835b --- /dev/null +++ b/backend/ServiceLayer.cs @@ -0,0 +1,51 @@ +public interface IUserService +{ + List GetAllCompaniesForUser(int userId); + + List GetAllProjectsForUser(int userId); + + List GetAllAvailableResourcesForUser(int userId); +} + +public interface ICompanyService +{ + string GetDatabaseUrl(int companyId); +} + +public interface IProjectService +{ + List GetAllTasksForProject(int projectId); +} + +public interface IResourceService +{ + void UpdateResourceAvailability(int resourceId, bool isAvailable); +} + +public interface IUserTaskService +{ + void CreateUserTask(int userId, int taskId, int resourceId, DateTime checkInTime); + + int GetUserTaskId(int userId, int taskId, int resourceId, bool taskCompleted); + + void UpdateUserTaskCompletion(int userTaskId, DateTime checkOutTime, bool taskCompleted); +} + +// Client activity sequence for the api calls, +// 1. userService.GetAllCompaniesForUser() +// 2. companyService.GetDatabaseUrl() + +// subsequent queries will go the the database server with the URL in step2. +// 3. userService.GetAllProjectsForUser() +// 4. userService.GetAllAvailableResourcesForUser() +// 5. projectService.GetAllTasksForProject() + +// Check-in +// 6. userTaskService.CreateUserTask() +// 7. resourceService.UpdateResourceAvailability(false) + +// Check-out +// 8. userTaskService.GetUserTaskId() +// 9. userTaskService.UpdateUserTaskCompletion() +// 10. resourceService.UpdateResourceAvailability(true) + diff --git a/backend/er_diagram.png b/backend/er_diagram.png new file mode 100644 index 0000000..e5964d9 Binary files /dev/null and b/backend/er_diagram.png differ diff --git a/ditioas.sql b/ditioas.sql new file mode 100644 index 0000000..d3775ff --- /dev/null +++ b/ditioas.sql @@ -0,0 +1,152 @@ +CREATE TABLE users ( + user_id SERIAL PRIMARY KEY, + user_name VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL, + password VARCHAR(255) NOT NULL +); + +CREATE TABLE projects ( + project_id SERIAL PRIMARY KEY, + project_name VARCHAR(50) NOT NULL, + description TEXT +); + +CREATE TABLE tasks ( + task_id SERIAL PRIMARY KEY, + task_name VARCHAR(50) NOT NULL, + description TEXT, + project_id INTEGER NOT NULL, + FOREIGN KEY (project_id) REFERENCES projects(project_id) +); + +CREATE TABLE resources ( + resource_id SERIAL PRIMARY KEY, + resource_name VARCHAR(50) NOT NULL, + resource_available BOOLEAN DEFAULT true +); + +CREATE TABLE companies ( + company_id SERIAL PRIMARY KEY, + company_name VARCHAR(50) NOT NULL, + url TEXT NOT NULL, +); + +CREATE TABLE admins ( + admin_id SERIAL PRIMARY KEY, + admin_name VARCHAR(50) NOT NULL +); + +CREATE TABLE user_works_for_company ( + user_id INTEGER NOT NULL, + company_id INTEGER NOT NULL, + PRIMARY KEY (user_id, company_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (company_id) REFERENCES companies(company_id) +); + +CREATE TABLE user_scheduled_for_project ( + user_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + PRIMARY KEY (user_id, project_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (project_id) REFERENCES projects(project_id) +); + +CREATE TABLE user_scheduled_for_resources ( + user_id INTEGER NOT NULL, + resource_id INTEGER NOT NULL, + PRIMARY KEY (user_id, resource_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (resource_id) REFERENCES resources(resource_id) +); + +CREATE TABLE user_tasks ( + user_task_id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + task_id INTEGER NOT NULL, + resource_id INTEGER, + check_in_time TIMESTAMP, + check_out_time TIMESTAMP, + task_completed BOOLEAN DEFAULT false, + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (task_id) REFERENCES tasks(task_id), + FOREIGN KEY (resource_id) REFERENCES resources(resource_id) +); + + +-- 1. user Logins +SELECT * +FROM users +WHERE users.email = 'RAHUL@GMAIL.COM' AND password = 'HASH_PASSWORD'; + +-- 2. user chooses a company +SELECT company_id, company_name +FROM companies +WHERE company_id IN ( + SELECT company_id + FROM user_works_for_company + WHERE user_id = 1234 +); +-- let the company_id is company_123 + +-- we pick the database url to which the subsequent queries should hit, lets say we maintain a cache +-- server with redis having (key, value) = (company_id, database_server_url) +-- let say it looks like following, +-- company_database_map = { +-- "ditioas" : "https://ditioas.mysql.database.azure.com", +-- "company_123" : "https://company_123.mysql.database.azure.com", +-- "vassabank" : "https://vassabank.mysql.database.azure.com", +-- } +-- so the following queries will go to "https://company_123.mysql.database.azure.com". + +-- 3. user gets a list of project which are scheduled for them and selects one project. +SELECT project_id, project_name, description +FROM projects +WHERE project_id IN ( + SELECT project_id + FROM user_scheduled_for_project + WHERE user_id = 1234 +); +-- let user chooses a project with project_id = 1000 + + +-- 4. user gets a list of resources which are scheduled for them and selects one resource. +SELECT resource_id, resource_name +FROM resources +WHERE resource_available = true +AND resource_id IN ( + SELECT resource_id + FROM user_scheduled_for_resources + WHERE user_id = 1234 +); +-- let user chooses a resource with resource_id = 2000 + +-- 5. user get a list of tasks belonging to the chosen project and chooses one task. +SELECT task_id, task_name, description +FROM tasks +WHERE project_id = 1000; +--let user chooses a task with task_id = 3000 + +-- 6. user clicks on check-in and records the check-in time and makes the resource unavailable. +INSERT INTO user_tasks (user_id, task_id, resource_id, check_in_time) +VALUES (1234, 3000, 2000, '2023-01-25 09:00:00'); +--let user_task_id = 4000 + +UPDATE resources +SET resource_available = false +WHERE resource_id = 2000; + +-- 7. user clicks on check-out and records the check-out time and makes the resource available. +SELECT user_task_id +FROM user_tasks +WHERE user_id = 1234 AND task_id = 3000 AND resource_id = 2000 AND task_completed = false; + +UPDATE user_tasks +SET check_out_time = '2023-01-25 12:00:00', task_completed = true +WHERE user_task_id = 4000; + +UPDATE resources +SET resource_available = true +WHERE resource_id = 2000; + +