Notice: This project is a REST API that is ment to work with frontend GUI
The sole purpose of this project is to fullfill requirements of Riverdi stakeholders (Riverdi Sales Department) in terms of creating an application for convienient RFQ (Request For Quotation) mangement and integrate it with current workflows that are run in Comarch ERP XL, ClickUp and SharePoint.
- Add new RFQs
- Generate unique RFQ code (based on specification)
- list RFQs
- sort RFQs (handled on frontend)
- filter RFQs (handled on frontend)
- Add requirements (with notes) to existing RFQs
- All
CRUD
operations on: RFQs, requirements, distributors and users - Comarch ERP XL integration (fetching customers)
- SharePoint Integration (creating specified folder structure, puting files)
- ClickUp Integration (creating tasks, assigning people, fetching task statuses)
In order to tie the database structure to the code (helpful when working with other engineers) I had to use schema migrations
(code that describes a precise change to make to the database).
The tool of the choice is node-pg-migrate. All migratrons are written in plain SQL.
What are
schema migrations
and how to use node-pg-migrate is out of the scope of this document.
I've created separate layer of utility methods to handle database operations.
They should be used in controller business logic instead of operating directly on database.
UserRepo.find();
[{id, username, name, email, shortname, role_id}]
Returns list of all users that are not admins and are not marked as
deleted
.
UserRepo.findWithAdmins();
[{id, username, name, email, shortname, role_id}]
Returns list of all users (admins included) that are not marked as
deleted
.
UserRepo.findById(id: string);
{id, username, email, shortname, role_id}
Returns user with matching
id
(or returns empty object).
UserRepo.findByEmail(email: string);
{id, username, email, password, shortname, role_id, deleted}
Returns user with matching email (or returns empty object). Returned hashed password is for login logic purposes.
UserRepo.insert({
username,
password,
email,
shortname,
role_id,
}: {
username: string;
password: string;
email: string;
shortname: string;
role_id: string;
});
{id, username, email, shortname, role_id}
Inserts new user into database. Shortname and email have to be unique. Password should be stored in a hashed form! (route controller logic should handle that)
UserRepo.updateData({
id,
username,
email,
shortname,
role_id,
}: {
id: string;
username: string;
email: string;
shortname: string;
role_id: string;
});
{id, username, email, shortname, role_id}
Updates existing user's data (only username, email, shortname and role_id). Shortname and email have to be unique. ID is immutable. Changing password is handled by dedicated method.
UserRepo.updatePassword({
id,
password,
}: {
id: string;
password: string;
});
{id, username, email, shortname, role_id}
Updates existing user's password. Password should be provided in a hashed form! (route controller logic should handle that)
UserRepo.delete(id: string);
{id, username, email, role_id, shortname}
Removes user from database.
UserRepo.markDeleted(id: string)
{id, username, email, role_id, shortname}
Marks user ad "deleted". Disables user (from login, from preforming actions, from appearing on all kings of listings) but keeps all the data (transaction history, etc.)
UserRepo.markUndeleted(id: string)
{id, username, email, role_id, shortname}
Sets "deleted" flag to
false
(enables user).
UserRepo.count()
Returns number of users.
RfqRepo.find();
[{id, rfq_code, eau, customer, distributor, pm, kam, updated}]
Returns list of all rfqs
RfqRepo.findById(id: string);
{
id,
rfq_code,
eau,
clickup_id,
customer_id,
customer,
distributor_id,
distributor,
pm_id,
pm,
pm_fullname,
kam_id,
kam,
kam_fullname,
final_solutions,
conclusions,
samples_expected,
mp_expected,
eau_max,
updated
}
Returns RFQ with matching
id
.
RfqRepo.findByDistributorId(distributor_id: string)
[{id, rfq_code}]
Returns list of all rfqs of distributor with matching
distributor_id
.
RfqRepo.findByRfqCode(rfq_code: string);
{id, rfq_code}
Returns RFQ with matching
id
.
RfqRepo.insert({
rfq_code,
eau,
customer_id,
distributor_id,
pm_id,
kam_id,
clickup_id,
final_solutions,
conclusions,
samples_expected,
mp_expected,
eau_max,
}: {
rfq_code: string;
eau: string;
customer_id: string;
distributor_id: string;
pm_id: string;
kam_id: string;
clickup_id: string;
final_solutions: string;
conclusions: string;
samples_expected: string;
mp_expected: string;
eau_max: string;
});
{id, rfq_code}
Inserts new RFQ into database.
rfq_code
have to be unique.
RfqRepo.updateData({
id,
eau,
customer_id,
distributor_id,
pm_id,
kam_id,
final_solutions,
conclusions,
samples_expected,
mp_expected,
eau_max,
}: {
id: string;
eau: string;
customer_id: string;
distributor_id: string;
pm_id: string;
kam_id: string;
final_solutions: string;
conclusions: string;
samples_expected: string;
mp_expected: string;
eau_max: string;
});
{id, rfq_code}
Updates RFQ into database.
rfq_code
have to be unique.
RfqRepo.delete(id: string);
{id, rfq_code}
Removes rfq from database.
RfqRepo.count()
Returns number of rfqs.
RequirementRepo.find();
[{id, rfq_id, priority, c_nc_cwr, requirement, note, date }]
Returns list of all requirements
RequirementRepo.findByRfqId(rfq_id: string);
[{id, rfq_id, priority, c_nc_cwr, requirement, note, date }]
Returns list of all requirements for given
rfq_id
RequirementRepo.findById(id: string);
{id, rfq_id, priority, c_nc_cwr, requirement, note, date }
Returns requirement with matching
id
RequirementRepo.insert({
rfq_id,
priority,
c_nc_cwr,
requirement,
note,
}: {
rfq_id: string;
priority: string;
c_nc_cwr: string;
requirement: string;
note: string;
});
{id, rfq_id}
Inserts new requirement into database.
RequirementRepo.updateData({
id,
rfq_id,
priority,
c_nc_cwr,
requirement,
note,
}: {
id: string;
rfq_id: string;
priority: string;
c_nc_cwr: string;
requirement: string;
note: string;
});
{id, rfq_id, c_nc_cwr, requirement, note}
Updates requirement with given
id
in the database.
RequirementRepo.delete(id: string);
{id}
Removes requirement from database.
RequirementRepo.count()
Returns number of requirements.
DistributorRepo.find();
[{id, name }]
Returns list of all distributors
DistributorRepo.findById(id: string);
{id, name }
Returns distributor with matching
id
DistributorRepo.findByName(name: string);
{id, name }
Returns distributor with matching
name
DistributorRepo.insert({ name }: { name: string });
{id, name}
Inserts new distributor into database.
DistributorRepo.updateData({ id, name }: { id: string; name: string });
{id, name}
Updates distributor with given
id
in the database.
DistributorRepo.delete(id: string);
{id, name}
Removes distributor from database.
DistributorRepo.count()
Returns number of distributors.
CustomerRepo.find();
[{id, name }]
Returns list of all customers
CustomerRepo.findById(id: string);
{id, name }
Returns customer with matching
id
CustomerRepo.count()
Returns number of customers.
Request:
POST
Request Body:
{email, password}
Response:
{ id, username, email, shortname, role_id, deleted }
+Set-Cookie
with user inJWT
Feature: allows existing user to "log in" - sends back user details in response body, starts cookie session and sends back user in JWT (JSON Web Token) stored in a cookie
Request:
POST
Request Body:
{ email, password, passwordConfirm, username, shortname, role_id }
Request Cookie:
currentUser in JWT
Response:
{ id, username, email, shortname, role_id }
Feature: create a new user with data provided in the request's body (important! New users can be added only by admins β standard user would get 401 error with message: "Not authorized" )
Request:
POST
Request Body:
{ email, password, passwordConfirm, username, shortname, role_id }
Response:
{ id, username, email, shortname, role_id }
Feature: logs out user - clears session
Request:
GET
Request Cookie:
currentUser in JWT
Response:
{ id, username, email, shortname, role_id }
Feature: returns user's id, username, email, shortname, role_id
for given id
git flow
server side sorting + data pagination
Abstract of the project or small introduction of what the project is about