diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..76246c6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +build +**/CMakeCache.txt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..752ea4b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @charankamarapu diff --git a/.gitmodules b/.gitmodules index f4b271e..497bb62 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,9 @@ [submodule "third_party/libbcrypt"] path = third_party/libbcrypt url = https://github.com/trusch/libbcrypt +[submodule "third_party/gtest"] + path = third_party/gtest + url = https://github.com/google/googletest +[submodule "third_party/gmock"] + path = third_party/gmock + url = https://github.com/google/googlemock diff --git a/CMakeLists.txt b/CMakeLists.txt index 44a3e69..c0929ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,83 +1,170 @@ +# cmake_minimum_required(VERSION 3.5) +# project(org_chart CXX) + +# include(CheckIncludeFileCXX) + +# check_include_file_cxx(any HAS_ANY) +# check_include_file_cxx(string_view HAS_STRING_VIEW) +# check_include_file_cxx(coroutine HAS_COROUTINE) +# if (NOT "${CMAKE_CXX_STANDARD}" STREQUAL "") +# # Do nothing +# elseif (HAS_ANY AND HAS_STRING_VIEW AND HAS_COROUTINE) +# set(CMAKE_CXX_STANDARD 20) +# elseif (HAS_ANY AND HAS_STRING_VIEW) +# set(CMAKE_CXX_STANDARD 17) +# else () +# set(CMAKE_CXX_STANDARD 14) +# endif () + +# set(CMAKE_CXX_STANDARD_REQUIRED ON) +# set(CMAKE_CXX_EXTENSIONS OFF) + +# add_executable(${PROJECT_NAME} main.cc) + +# # Add these lines for coverage +# if (COVERAGE MATCHES "ON") # Or any build type you use for coverage +# message(STATUS "Enabling code coverage flags") +# target_compile_options(${PROJECT_NAME} PRIVATE --coverage) +# target_link_options(${PROJECT_NAME} PRIVATE --coverage) +# endif() + +# # ############################################################################## +# # https://github.com/drogonframework/drogon +# add_subdirectory(third_party/drogon) +# target_link_libraries(${PROJECT_NAME} PRIVATE drogon) + +# # https://github.com/Thalhammer/jwt-cpp +# add_subdirectory(third_party/jwt-cpp) +# target_link_libraries(${PROJECT_NAME} PRIVATE jwt-cpp) + +# # https://github.com/trusch/libbcrypt +# add_subdirectory(third_party/libbcrypt) +# target_link_libraries(${PROJECT_NAME} PRIVATE bcrypt) + +# add_subdirectory(third_party/gtest) +# target_link_libraries(${PROJECT_NAME} PRIVATE gtest gtest_main) + +# # and comment out the following lines +# find_package(Drogon CONFIG REQUIRED) +# target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon) + +# # ############################################################################## + +# if (CMAKE_CXX_STANDARD LESS 17) +# # With C++14, use boost to support any, string_view and filesystem +# message(STATUS "use c++14") +# find_package(Boost 1.61.0 REQUIRED) +# target_link_libraries(${PROJECT_NAME} PUBLIC Boost::boost) +# elseif (CMAKE_CXX_STANDARD LESS 20) +# message(STATUS "use c++17") +# else () +# message(STATUS "use c++20") +# endif () + +# aux_source_directory(controllers CTL_SRC) +# aux_source_directory(filters FILTER_SRC) +# aux_source_directory(plugins PLUGIN_SRC) +# aux_source_directory(models MODEL_SRC) +# aux_source_directory(utils UTIL_SRC) + +# target_include_directories(${PROJECT_NAME} +# PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +# ${CMAKE_CURRENT_SOURCE_DIR}/models) +# target_sources(${PROJECT_NAME} +# PRIVATE +# ${SRC_DIR} +# ${CTL_SRC} +# ${FILTER_SRC} +# ${PLUGIN_SRC} +# ${MODEL_SRC} +# ${UTIL_SRC}) +# # ############################################################################## +# # uncomment the following line for dynamically loading views +# # set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON) + +# # ############################################################################## + +# add_subdirectory(test) + +# # add_executable(${PROJECT_NAME}_test test/test_main.cc) + +# # target_link_libraries(${PROJECT_NAME}_test PRIVATE drogon) + +# # ParseAndAddDrogonTests(${PROJECT_NAME}_test) +# # =================================================================== +# # AFL++ Fuzzer Target (add this entire block to the end of the file) +# # =================================================================== +# option(BUILD_FUZZER "Build the AFL++ fuzzer harness" ON) + +# if(BUILD_FUZZER) +# message(STATUS "Fuzzer build is enabled.") + +# # Force the use of the AFL++ compiler, overriding any external settings. +# set(CMAKE_CXX_COMPILER "afl-clang-fast++" CACHE FILEPATH "AFL++ C++ compiler" FORCE) +# set(CMAKE_C_COMPILER "afl-clang-fast" CACHE FILEPATH "AFL++ C compiler" FORCE) + +# # Define the fuzzer executable +# add_executable(fuzz_harness harness.cpp) + +# # Link the fuzzer against your entire API project. +# # This automatically handles ALL dependencies, source files, and libraries. +# target_link_libraries(fuzz_harness PRIVATE org_chart) + +# # Add the FUZZING_BUILD definition and the fuzzer runtime linker flag +# target_compile_definitions(fuzz_harness PRIVATE FUZZING_BUILD) +# target_link_libraries(fuzz_harness PRIVATE -fsanitize=fuzzer) + +# message(STATUS "Configured fuzzer target. To build, run from your build directory: make fuzz_harness") +# endif() cmake_minimum_required(VERSION 3.5) project(org_chart CXX) -include(CheckIncludeFileCXX) - -check_include_file_cxx(any HAS_ANY) -check_include_file_cxx(string_view HAS_STRING_VIEW) -check_include_file_cxx(coroutine HAS_COROUTINE) -if (NOT "${CMAKE_CXX_STANDARD}" STREQUAL "") - # Do nothing -elseif (HAS_ANY AND HAS_STRING_VIEW AND HAS_COROUTINE) - set(CMAKE_CXX_STANDARD 20) -elseif (HAS_ANY AND HAS_STRING_VIEW) - set(CMAKE_CXX_STANDARD 17) -else () - set(CMAKE_CXX_STANDARD 14) -endif () - +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -add_executable(${PROJECT_NAME} main.cc) - -# ############################################################################## -# https://github.com/drogonframework/drogon add_subdirectory(third_party/drogon) -target_link_libraries(${PROJECT_NAME} PRIVATE drogon) - -# https://github.com/Thalhammer/jwt-cpp add_subdirectory(third_party/jwt-cpp) -target_link_libraries(${PROJECT_NAME} PRIVATE jwt-cpp) - -# https://github.com/trusch/libbcrypt add_subdirectory(third_party/libbcrypt) -target_link_libraries(${PROJECT_NAME} PRIVATE bcrypt) - -# and comment out the following lines -find_package(Drogon CONFIG REQUIRED) -target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon) - -# ############################################################################## - -if (CMAKE_CXX_STANDARD LESS 17) - # With C++14, use boost to support any, string_view and filesystem - message(STATUS "use c++14") - find_package(Boost 1.61.0 REQUIRED) - target_link_libraries(${PROJECT_NAME} PUBLIC Boost::boost) -elseif (CMAKE_CXX_STANDARD LESS 20) - message(STATUS "use c++17") -else () - message(STATUS "use c++20") -endif () +add_subdirectory(third_party/jsoncpp) aux_source_directory(controllers CTL_SRC) -aux_source_directory(filters FILTER_SRC) -aux_source_directory(plugins PLUGIN_SRC) aux_source_directory(models MODEL_SRC) aux_source_directory(utils UTIL_SRC) -target_include_directories(${PROJECT_NAME} - PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/models) -target_sources(${PROJECT_NAME} - PRIVATE - ${SRC_DIR} - ${CTL_SRC} - ${FILTER_SRC} - ${PLUGIN_SRC} - ${MODEL_SRC} - ${UTIL_SRC}) -# ############################################################################## -# uncomment the following line for dynamically loading views -# set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON) +add_library(org_chart_lib STATIC + ${CTL_SRC} + ${MODEL_SRC} + ${UTIL_SRC} +) + +target_link_libraries(org_chart_lib PRIVATE + drogon + jwt-cpp + bcrypt + jsoncpp_lib +) +target_include_directories(org_chart_lib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) +add_executable(${PROJECT_NAME} main.cc) +target_link_libraries(${PROJECT_NAME} PRIVATE org_chart_lib) -# ############################################################################## +option(BUILD_FUZZER "Build the AFL++ fuzzer harness" ON) -add_subdirectory(test) +if(BUILD_FUZZER) + message(STATUS "Fuzzer build is enabled.") + add_executable(fuzz_harness harness.cpp) -# add_executable(${PROJECT_NAME}_test test/test_main.cc) + target_link_libraries(fuzz_harness PRIVATE org_chart_lib) -# target_link_libraries(${PROJECT_NAME}_test PRIVATE drogon) + target_include_directories(fuzz_harness PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/drogon/lib/inc + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/drogon/orm_lib/inc + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/jsoncpp/include + ) + target_link_options(fuzz_harness PRIVATE -fsanitize=fuzzer) -# ParseAndAddDrogonTests(${PROJECT_NAME}_test) + target_compile_definitions(fuzz_harness PRIVATE FUZZING_BUILD) + message(STATUS "Configured fuzzer target. To build, run: make fuzz_harness") +endif() \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a7d2cc0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# Start with the base Ubuntu image +FROM ubuntu:22.04 + +# Set the timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Install necessary dependencies +RUN apt-get update -yqq \ + && apt-get install -yqq --no-install-recommends \ + software-properties-common \ + sudo curl wget cmake make pkg-config locales git \ + gcc-11 g++-11 openssl libssl-dev libjsoncpp-dev uuid-dev \ + zlib1g-dev libc-ares-dev postgresql-server-dev-all \ + libmariadb-dev libsqlite3-dev libhiredis-dev \ + && rm -rf /var/lib/apt/lists/* \ + && locale-gen en_US.UTF-8 + +# Set environment variables for localization +ENV LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 \ + CC=gcc-11 \ + CXX=g++-11 \ + AR=gcc-ar-11 \ + RANLIB=gcc-ranlib-11 \ + IROOT=/install + +# Clone Drogon repository +ENV DROGON_ROOT="$IROOT/drogon" +RUN git clone --depth 1 --recurse-submodules \ + https://github.com/drogonframework/drogon $DROGON_ROOT # ← submodules pulled + +WORKDIR $DROGON_ROOT +RUN mkdir build && cd build && \ + cmake .. -DCMAKE_BUILD_TYPE=Release \ + -DMYSQL_CLIENT=ON \ + -DPOSTGRESQL_CLIENT=OFF \ + && make -j$(nproc) && make install + +# Build Drogon +RUN ./build.sh + +WORKDIR / + +# Copy source code for your application (from the local directory) +COPY . /app + +WORKDIR /app + +# Install build tools for the app +RUN apt-get update && apt-get install -y cmake g++ git + +# Pull submodules for your application +RUN git submodule update --init --recursive + +# Create build directory and build the project +RUN mkdir -p /app/build && cd /app/build && cmake .. && make -j$(nproc) + +# Set CMD to the actual binary +CMD ["./build/org_chart"] diff --git a/ReadMe.md b/ReadMe.md index e16ea63..a9713fb 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,189 +1,347 @@ -# Org Chart Api - -Restful API built using -[Drogon](https://github.com/drogonframework/drogon).
-Routes are protected using JWT for token-based authorization. - -Endpoints ---------- - -### Persons -| Method | URI | Action | -|------------|---------------------------------------|-------------------------------------------| -| `GET` | `/persons?limit={}&offset={}&sort_field={}&sort_order={}` | `Retrieve all persons` | -| `GET` | `/persons/{id}` | `Retrieve person` | -| `GET` | `/persons/{id}/reports` | `Retrieve person direct reports` | -| `POST` | `/persons` | `Create person` | -| `PUT` | `/persons/{id}` | `Update person` | -| `DELETE` | `/persons/{id} ` | `Delete person` | - -### Departments -| Method | URI | Action | -|------------|---------------------------------------|-------------------------------------------| -| `GET` | `/departments?limit={}&offset={}&sort_field={}&sort_order={}` | `Retrieve all departments` | -| `GET` | `/departments/{id}` | `Retrieve department` | -| `GET` | `/departments/{id}/persons` | `Retrieve department persons` | -| `POST` | `/departments` | `Create department` | -| `PUT` | `/departments/{id}` | `Update department` | -| `DELETE` | `/departments/{id}` | `Delete department` | - -### Jobs -| Method | URI | Action | -|------------|---------------------------------------|-------------------------------------------| -| `GET` | `/jobs?limit={}&offset={}&sort_fields={}&sort_order={}` | `Retrieve all jobs` | -| `GET` | `/jobs/{id}` | `Retrieve job` | -| `GET` | `/jobs/{id}/persons` | `Retrieve job persons` | -| `POST` | `/jobs` | `Create job` | -| `PUT` | `/jobs/{id}` | `Update job` | -| `DELETE` | `/jobs/{id}` | `Delete job` | - -### Auth -| Method | URI | Action | -|------------|---------------------------------------|-------------------------------------------| -| `POST` | `/auth/register` | `Register user and obtain JWT token` | -| `POST` | `/auth/login` | `Login User ` | - -How to build the project +# Org Chart API + +## πŸ“‘ Index + +1. [Overview](#overview) +2. [Endpoints](#-endpoints) + - [Persons](#persons) + - [Departments](#-departments) + - [Jobs](#-jobs) + - [Auth](#-auth) +3. [Getting Started](#-two-ways-to-get-started) + - [Using Docker](#1-using-docker) + - [Manual Setup](#2-manual-setup-for-those-who-prefer-to-run-the-project-locally) + - [Install Dependencies](#-install-dependencies) + - [Drogon Installation](#-drogon-installation) + - [Database Setup](#database-setup) + - [Build the Project](#build-the-project) +4. [UT and Coverage](#-ut-and-coverage) +5. [Usage Guide](#-usage-guide) +6. [keploy Integration and Coverage](#keploy-integration-api-testing-and-coverage) + +## Overview + +A **RESTful API** built with [Drogon](https://github.com/drogonframework/drogon), a high-performance C++ framework. This API is designed to manage organizational structures, including persons, departments, and job roles. + +πŸ” **All routes are protected using JWT for token-based authentication**. + +## πŸ“š Endpoints + +### 🧍 Persons + +| Method | URI | Action | +| -------- | --------------------------------------------------------- | ------------------------- | +| `GET` | `/persons?limit={}&offset={}&sort_field={}&sort_order={}` | Retrieve all persons | +| `GET` | `/persons/{id}` | Retrieve a single person | +| `GET` | `/persons/{id}/reports` | Retrieve direct reports | +| `POST` | `/persons` | Create a new person | +| `PUT` | `/persons/{id}` | Update a person's details | +| `DELETE` | `/persons/{id}` | Delete a person | + --- -### Installation -See drogon documentation [here](https://github.com/an-tao/drogon/wiki/ENG-02-Installation#System-Requirements)! - -### Verify Installation -Confirm the database development environment using `drogon_ctl -v`: -``` - _ [0/365] - __| |_ __ ___ __ _ ___ _ __ - / _` | '__/ _ \ / _` |/ _ \| '_ \ -| (_| | | | (_) | (_| | (_) | | | | - \__,_|_| \___/ \__, |\___/|_| |_| - |___/ - -A utility for drogon -Version: 1.7.5 -Git commit: fc68b8c92c8c202d8cc58d83629d6e8c8701fc47 -Compilation: - Compiler: /Library/Developer/CommandLineTools/usr/bin/c++ - Compiler ID: AppleClang - Compilation flags: -std=c++17 -I/usr/local/include -Libraries: - postgresql: yes (batch mode: no) - mariadb: yes - sqlite3: yes - openssl: yes - brotli: yes - boost: no - hiredis: no - c-ares: yes -``` - -### Setup Database -Start a postgres server.
-`docker run --name pg -e POSTGRES_PASSWORD=password -d -p 5433:5432 postgres` - - -Log into postgres using `psql` to create a `org_chart` database.
-`psql 'postgresql://postgres:password@127.0.0.1:5433/org_chart'` - -Create and seed the tables.
-`psql 'postgresql://postgres:password@127.0.0.1:5433/org_chart' -f scripts/create_db.sql`
-`psql 'postgresql://postgres:password@127.0.0.1:5433/org_chart' -f scripts/seed_db.sql` - -### Build -``` -git clone https://github.com/maikeulb/orgChartApi -git submodule update --init -mkdir build -cd build + +### 🏒 Departments + +| Method | URI | Action | +| -------- | ------------------------------------------------------------- | --------------------------- | +| `GET` | `/departments?limit={}&offset={}&sort_field={}&sort_order={}` | Retrieve all departments | +| `GET` | `/departments/{id}` | Retrieve a department | +| `GET` | `/departments/{id}/persons` | Retrieve department members | +| `POST` | `/departments` | Create a department | +| `PUT` | `/departments/{id}` | Update department info | +| `DELETE` | `/departments/{id}` | Delete a department | + +--- + +### πŸ’Ό Jobs + +| Method | URI | Action | +| -------- | ------------------------------------------------------- | ----------------------------- | +| `GET` | `/jobs?limit={}&offset={}&sort_fields={}&sort_order={}` | Retrieve all job roles | +| `GET` | `/jobs/{id}` | Retrieve a job role | +| `GET` | `/jobs/{id}/persons` | Retrieve people in a job role | +| `POST` | `/jobs` | Create a job role | +| `PUT` | `/jobs/{id}` | Update job role | +| `DELETE` | `/jobs/{id}` | Delete a job role | + +--- + +### πŸ” Auth + +| Method | URI | Action | +| ------ | ---------------- | ----------------------------------- | +| `POST` | `/auth/register` | Register a user and get a JWT token | +| `POST` | `/auth/login` | Login and receive a JWT token | + +--- + +## πŸ“¦ Two Ways to Get Started + +There are two ways to run the project: + +### 1. Using Docker + +**in `config.json` file change the host from `127.0.0.1` to db** + +```bash +docker compose up +``` + +Docker simplifies the setup process and ensures all dependencies are handled automatically. + +### 2. **Manual Setup** (For those who prefer to run the project locally) + +### πŸ“₯ Install Dependencies + +```bash +sudo apt-get update -yqq \ + && sudo apt-get install -yqq --no-install-recommends \ + software-properties-common \ + curl wget cmake make pkg-config locales git \ + gcc-11 g++-11 openssl libssl-dev libjsoncpp-dev uuid-dev \ + zlib1g-dev libc-ares-dev postgresql-server-dev-all \ + libmariadb-dev libsqlite3-dev libhiredis-dev \ + && sudo rm -rf /var/lib/apt/lists/* +``` + +### πŸ‰ Drogon Installation + +```bash +DROGON_ROOT="$HOME/drogon" +``` + +```bash +git clone --depth 1 --recurse-submodules https://github.com/drogonframework/drogon $DROGON_ROOT +``` + +```bash +cd $HOME/drogon +``` + +```bash +mkdir build && cd build +``` + +```bash +cmake .. -DCMAKE_BUILD_TYPE=Release -DUSE_MYSQL=ON +``` + +```bash +make -j$(nproc) && sudo make install +``` + +```bash +drogon_ctl -v +``` + +### Database Setup + +```bash +navigate to orgchartAPI repo folder +``` + +```bash +docker run --name db \ + -e MYSQL_ROOT_PASSWORD=password \ + -e MYSQL_DATABASE=org_chart \ + -e MYSQL_USER=org \ + -e MYSQL_PASSWORD=password \ + -p 3306:3306 \ + -d mysql:8.3 \ + --default-authentication-plugin=mysql_native_password +``` + +```bash +sudo apt install default-mysql-client +``` + +```bash +mysql -h127.0.0.1 -P3306 -uorg -ppassword org_chart < scripts/create_db.sql +mysql -h127.0.0.1 -P3306 -uorg -ppassword org_chart < scripts/seed_db.sql +``` + +### Build the Project + +```bash +git submodule update --init --recursive +``` + +```bash +mkdir build && cd build +``` + +```bash cmake .. +``` + +```bash make ``` -### Run -Make the necessary database changes to `config.json` and run the project `./org_chart` - -Usage ---------------- -1. register user
-`http post localhost:3000/auth/register username="admin" password="password"` -``` -{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE2NDU4MzE2MDcsImlhdCI6MTY0NTgzMTYwNywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMCJ9.8PyNKVTlY6Qy81kXrCXTSD2XRxSKHLxmIELqEmOyFoU", - "username": "admin" -} -``` - -2. login user and obtain token (can also obtain token after registering)
-`http post localhost:3000/auth/login username="admin" password="password"` -``` -{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE2NDU4MzE2MDcsImlhdCI6MTY0NTgzMTYwNywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMCJ9.8PyNKVTlY6Qy81kXrCXTSD2XRxSKHLxmIELqEmOyFoU", - "username": "admin" -} -``` - -3. access resource using token
-`http --auth-type=bearer --auth="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE2NDU4MzE2MzYsImlhdCI6MTY0NTgzMTYzNiwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMyJ9.x84yaRyC8sxjfRqeBC9AJW4NUAA2nhDexFUh3lImF50" get localhost:3000/persons offset=1 limit=25 sort_field=id sort_order=asc` -``` -[ - { - "department": { - "id": 1, - "name": "Product" - }, - "first_name": "Gary", - "hire_date": "2018-04-07 01:00:00", - "id": 2, - "job": { - "id": 2, - "title": "M1" - }, - "last_name": "Reed", - "manager": { - "id": 1, - "full_name": "Sabryna Peers", - } - }, - { - "department": { - "id": 1, - "name": "Product" - }, - "first_name": "Madonna", - "hire_date": "2018-03-08", - "id": 3, - "job": { - "id": 2, - "title": "M1" - }, - "last_name": "Axl", - "manager": { - "id": 1, - "full_name": "Sabryna Peers", - } - }, - { - "department": { - "id": 1, - "name": "Product" - }, - "first_name": "Marcia", - "hire_date": "2020-01-11", - "id": 4, - "job": { - "id": 4, - "title": "E5" - }, - "last_name": "Stuart", - "manager": { - "id": 2, - "full_name": "Gary Reed", - } - }, -... -] -``` - -### Troubleshooting -* Ensure that openssl is installed correctly (check `drogon_ctl -v`) and point cmake to the correct directory.
- `cmake -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl` -* If you're using a LSP, export `compile_commands.json`
- `cmake -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl -DCMAKE_EXPORT_COMPILE_COMMANDS=` + +```bash +./org_chart +``` + +## πŸ§ͺ UT and Coverage + +There are already some unit tests in the repository. Here’s how you can run them and generate a coverage report: + +1. Install `gcovr` + + ```bash + sudo apt install gcovr + ``` + +2. Navigate to the orgChartAPI Repository + +3. Build the Project with Coverage Enabled + Follow the [Build the Project](#build-the-project) steps, but **replace**: + + ```bash + cmake .. + ``` + + with + + ```bash + cmake -DCOVERAGE=ON .. + ``` + +4. Run the Unit Tests + + ```bash + ./test/org_chart_test + ``` + +5. Navigate to the orgChartAPI Repository + +6. Generate the Coverage Report + ```bash + mkdir -p coverage + gcovr -r . --html --html-details -o coverage/coverage.html + ``` + +Open `coverage/coverage.html` in your browser to view the coverage report. + +## πŸ’‘ Usage Guide + +Use the `postman.json` for postman collection and try the requests + +## Keploy Integration (API Testing and Coverage) + +Integrate [Keploy](https://keploy.io) to automatically record, replay, and generate coverage for your API tests. + +--- + +### 1. Install Keploy + +**Open Source:** + +```bash +curl --silent -O -L https://keploy.io/install.sh && source install.sh +``` + +**Enterprise:** + +```bash +curl --silent -O -L https://keploy.io/ent/install.sh && source install.sh +``` + +--- + +### 2. Run Application in Record Mode + +**If using Docker Compose:** + +```bash +keploy record -c "docker compose up" --container-name "drogon_app" +``` + +**Or, if running manually:** + +```bash +keploy record -c "./org_chart" +``` + +--- + +### 3. Hit and Record API Requests + +Example (Register a new user): + +```bash +curl --location 'http://localhost:3000/auth/register' \ + --header 'Content-Type: application/json' \ + --data '{ + "username": "admin3adwes2", + "password": "password" + }' +``` + +--- + +### 4. Stop Keploy and Run in Test Mode + +**With Docker Compose:** + +```bash +keploy test -c "docker compose up" --container-name "drogon_app" +``` + +**Or, manually:** + +```bash +keploy test -c "./org_chart" +``` + +--- + +### 5. View Coverage Report + +Coverage will be **automatically saved** if the build was done with the `-DCOVERAGE=ON` flag during CMake. + +for combined coverage you can do something like this + +1. After you ran the uts save the coverage to a json + + ```bash + gcovr -r . --json ut.json + ``` + +2. remove the saved coverage object files + + ```bash + find . -name "*.gcda" -delete + ``` + +3. After you ran keploy test save the coverage to another json + + ```bash + gcovr -r . --json it.json + ``` + +4. Generate report for ut + + ```bash + gcovr --add-tracefile ut.json --html --html-details -o coverage-ut/coverage.html + ``` + +5. Generate report for it + + ```bash + gcovr --add-tracefile it.json --html --html-details -o coverage-it/coverage.html + ``` + +6. generate report for combined + + ```bash + gcovr merge --add-tracefile ut.json --add-tracefile it.json --json -o merged.json + ``` + + ```bash + gcovr --add-tracefile merged.json --html --html-details -o coverage-combined/coverage.html + ``` + +--- + +For more, see [Keploy Docs](https://docs.keploy.io/). diff --git a/config.json b/config.json index 5fe1536..136b324 100644 --- a/config.json +++ b/config.json @@ -3,80 +3,39 @@ { "listeners": [ { - //address: Ip address,0.0.0.0 by default "address": "0.0.0.0", - //port: Port number "port": 3000, - //https: If true, use https for security,false by default "https": false } ], "db_clients": [ { - //name: Name of the client,'default' by default //"name":"", //rdbms: Server type, postgresql,mysql or sqlite3, "postgresql" by default - "rdbms": "postgresql", - //filename: Sqlite3 db file name + "rdbms": "mysql", // ⬅️ switched to mysql //"filename":"", - //host: Server address,localhost by default "host": "127.0.0.1", - //port: Server port, 5432 by default - "port": 5433, - //dbname: Database name + //port: Server port, 3306 for MySQL + "port": 3306, // ⬅️ MySQL port "dbname": "org_chart", - //user: 'postgres' by default - "user": "postgres", - //passwd: '' by default + //user: root by default; use the app-specific user we created + "user": "org", // ⬅️ MySQL user "passwd": "password", - //is_fast: false by default, if it is true, the client is faster but user can't call - //any synchronous interface of it. "is_fast": false, - //client_encoding: The character set used by the client. it is empty string by default which - //means use the default character set. //"client_encoding": "", - //number_of_connections: 1 by default, if the 'is_fast' is true, the number is the number of - //connections per IO thread, otherwise it is the total number of all connections. "number_of_connections": 1, - //timeout: -1.0 by default, in seconds, the timeout for executing a SQL query. - //zero or negative value means no timeout. "timeout": -1.0 } ], "app": { - //number_of_threads: The number of IO threads, 1 by default, if the value is set to 0, the number of threads - //is the number of CPU cores "number_of_threads": 1, - //enable_session: False by default "enable_session": false, "session_timeout": 0, - //document_root: Root path of HTTP document, defaut path is ./ "document_root": "./", - //home_page: Set the HTML file of the home page, the default value is "index.html" - //If there isn't any handler registered to the path "/", the home page file in the "document_root" is send to clients as a response - //to the request for "/". "home_page": "index.html", - //use_implicit_page: enable implicit pages if true, true by default "use_implicit_page": true, - //implicit_page: Set the file which would the server access in a directory that a user accessed. - //For example, by default, http://localhost/a-directory resolves to http://localhost/a-directory/index.html. "implicit_page": "index.html", - //static_file_headers: Headers for static files - /*"static_file_headers": [ - { - "name": "field-name", - "value": "field-value" - } - ],*/ - //upload_path: The path to save the uploaded file. "uploads" by default. - //If the path isn't prefixed with /, ./ or ../, - //it is relative path of document_root path "upload_path": "uploads", - /* file_types: - * HTTP download file types,The file types supported by drogon - * by default are "html", "js", "css", "xml", "xsl", "txt", "svg", - * "ttf", "otf", "woff2", "woff" , "eot", "png", "jpg", "jpeg", - * "gif", "bmp", "ico", "icns", etc. */ "file_types": [ "gif", "png", @@ -91,151 +50,58 @@ "cur", "xml" ], - //locations: An array of locations of static files for GET requests. "locations": [ { - //uri_prefix: The URI prefix of the location prefixed with "/", the default value is "" that disables the location. //"uri_prefix": "/.well-known/acme-challenge/", - //default_content_type: The default content type of the static files without - //an extension. empty string by default. "default_content_type": "text/plain", - //alias: The location in file system, if it is prefixed with "/", it - //presents an absolute path, otherwise it presents a relative path to - //the document_root path. - //The default value is "" which means use the document root path as the location base path. "alias": "", - //is_case_sensitive: indicates whether the URI prefix is case sensitive. "is_case_sensitive": false, - //allow_all: true by default. If it is set to false, only static files with a valid extension can be accessed. "allow_all": true, - //is_recursive: true by default. If it is set to false, files in sub directories can't be accessed. "is_recursive": true, - //filters: string array, the filters applied to the location. "filters": [] } ], - //max_connections: maximum number of connections, 100000 by default "max_connections": 100000, - //max_connections_per_ip: maximum number of connections per clinet, 0 by default which means no limit "max_connections_per_ip": 0, - //Load_dynamic_views: False by default, when set to true, drogon - //compiles and loads dynamically "CSP View Files" in directories defined - //by "dynamic_views_path" "load_dynamic_views": false, - //dynamic_views_path: If the path isn't prefixed with /, ./ or ../, - //it is relative path of document_root path "dynamic_views_path": [ "./views" ], - //dynamic_views_output_path: Default by an empty string which means the output path of source - //files is the path where the csp files locate. If the path isn't prefixed with /, it is relative - //path of the current working directory. "dynamic_views_output_path": "", - //enable_unicode_escaping_in_json: true by default, enable unicode escaping in json. "enable_unicode_escaping_in_json": true, - //float_precision_in_json: set precision of float number in json. "float_precision_in_json": { - //precision: 0 by default, 0 means use the default precision of the jsoncpp lib. "precision": 0, - //precision_type: must be "significant" or "decimal", defaults to "significant" that means - //setting max number of significant digits in string, "decimal" means setting max number of - //digits after "." in string "precision_type": "significant" }, - //log: Set log output, drogon output logs to stdout by default "log": { - //log_path: Log file path,empty by default,in which case,logs are output to the stdout - //"log_path": "./", - //logfile_base_name: Log file base name,empty by default which means drogon names logfile as - //drogon.log ... "logfile_base_name": "", - //log_size_limit: 100000000 bytes by default, - //When the log file size reaches "log_size_limit", the log file is switched. "log_size_limit": 100000000, - //log_level: "DEBUG" by default,options:"TRACE","DEBUG","INFO","WARN" - //The TRACE level is only valid when built in DEBUG mode. "log_level": "DEBUG" }, - //run_as_daemon: False by default "run_as_daemon": false, - //handle_sig_term: True by default "handle_sig_term": true, - //relaunch_on_error: False by default, if true, the program will be restart by the parent after exiting; "relaunch_on_error": false, - //use_sendfile: True by default, if true, the program - //uses sendfile() system-call to send static files to clients; "use_sendfile": true, - //use_gzip: True by default, use gzip to compress the response body's content; "use_gzip": true, - //use_brotli: False by default, use brotli to compress the response body's content; "use_brotli": false, - //static_files_cache_time: 5 (seconds) by default, the time in which the static file response is cached, - //0 means cache forever, the negative value means no cache "static_files_cache_time": 5, - //simple_controllers_map: Used to configure mapping from path to simple controller - // "simple_controllers_map": [ - // { - // "path": "/path/name", - // "controller": "controllerClassName", - // "http_methods": [ - // "get", - // "post" - // ], - // "filters": [ - // "FilterClassName" - // ] - // } - // ], - //idle_connection_timeout: Defaults to 60 seconds, the lifetime - //of the connection without read or write "idle_connection_timeout": 60, - //server_header_field: Set the 'Server' header field in each response sent by drogon, - //empty string by default with which the 'Server' header field is set to "Server: drogon/version string\r\n" "server_header_field": "", - //enable_server_header: Set true to force drogon to add a 'Server' header to each HTTP response. The default - //value is true. "enable_server_header": true, - //enable_date_header: Set true to force drogon to add a 'Date' header to each HTTP response. The default - //value is true. "enable_date_header": true, - //keepalive_requests: Set the maximum number of requests that can be served through one keep-alive connection. - //After the maximum number of requests are made, the connection is closed. - //The default value of 0 means no limit. "keepalive_requests": 0, - //pipelining_requests: Set the maximum number of unhandled requests that can be cached in pipelining buffer. - //After the maximum number of requests are made, the connection is closed. - //The default value of 0 means no limit. "pipelining_requests": 0, - //gzip_static: If it is set to true, when the client requests a static file, drogon first finds the compressed - //file with the extension ".gz" in the same path and send the compressed file to the client. - //The default value of gzip_static is true. "gzip_static": true, - //br_static: If it is set to true, when the client requests a static file, drogon first finds the compressed - //file with the extension ".br" in the same path and send the compressed file to the client. - //The default value of br_static is true. "br_static": true, - //client_max_body_size: Set the maximum body size of HTTP requests received by drogon. The default value is "1M". - //One can set it to "1024", "1k", "10M", "1G", etc. Setting it to "" means no limit. "client_max_body_size": "1M", - //max_memory_body_size: Set the maximum body size in memory of HTTP requests received by drogon. The default value is "64K" bytes. - //If the body size of a HTTP request exceeds this limit, the body is stored to a temporary file for processing. - //Setting it to "" means no limit. "client_max_memory_body_size": "64K", - //client_max_websocket_message_size: Set the maximum size of messages sent by WebSocket client. The default value is "128K". - //One can set it to "1024", "1k", "10M", "1G", etc. Setting it to "" means no limit. "client_max_websocket_message_size": "128K", - //reuse_port: Defaults to false, users can run multiple processes listening on the same port at the same time. "reuse_port": false }, - //plugins: Define all plugins running in the application "plugins": [ { - //name: The class name of the plugin //"name": "drogon::plugin::SecureSSLRedirector", - //dependencies: Plugins that the plugin depends on. It can be commented out "dependencies": [], - //config: The configuration of the plugin. This json object is the parameter to initialize the plugin. - //It can be commented out "config": { "ssl_redirect_exempt": [ ".*\\.jpg" @@ -244,22 +110,16 @@ } }, { - //name: The class name of the plugin //"name": "JwtPlugin" - //dependencies: Plugins that the plugin depends on. It can be commented out "dependencies": [], - //config: The configuration of the plugin. This json object is the parameter to initialize the plugin. - //It can be commented out "config": { - "jwt-secret":"secret", - "jwt-sessionTime":3600 + "jwt-secret": "secret", + "jwt-sessionTime": 3600 } } - ], - //custom_config: custom configuration for users. This object can be get by the app().getCustomConfig() method. "custom_config": { - "jwt-secret":"secret", - "jwt-sessionTime":3600 + "jwt-secret": "secret", + "jwt-sessionTime": 3600 } } diff --git a/controllers/AuthController.cc b/controllers/AuthController.cc index c1b1952..bd5d16e 100644 --- a/controllers/AuthController.cc +++ b/controllers/AuthController.cc @@ -5,23 +5,38 @@ using namespace drogon::orm; using namespace drogon_model::org_chart; -namespace drogon { - template<> - inline User fromRequest(const HttpRequest &req) { +namespace drogon +{ + template <> + inline User fromRequest(const HttpRequest &req) + { auto jsonPtr = req.getJsonObject(); - auto json = *jsonPtr; - auto user = User(json); - return user; + if (jsonPtr) + { + return User(*jsonPtr); + } + + Json::Value json; + Json::Reader reader; + if (reader.parse(std::string(req.body()), json)) + { // <-- FIXED + return User(json); + } + + return User(Json::Value{}); } } -void AuthController::registerUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const { +void AuthController::registerUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const +{ LOG_DEBUG << "registerUser"; - try { + try + { auto dbClientPtr = drogon::app().getDbClient(); Mapper mp(dbClientPtr); - if (!areFieldsValid(pUser)) { + if (!areFieldsValid(pUser)) + { Json::Value ret{}; ret["error"] = "missing fields"; auto resp = HttpResponse::newHttpJsonResponse(ret); @@ -30,7 +45,8 @@ void AuthController::registerUser(const HttpRequestPtr &req, std::functionsetStatusCode(HttpStatusCode::k201Created); callback(resp); - } catch (const DrogonDbException & e) { + } + catch (const DrogonDbException &e) + { LOG_ERROR << e.base().what(); Json::Value ret{}; ret["error"] = "database error"; @@ -58,13 +76,16 @@ void AuthController::registerUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const { +void AuthController::loginUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const +{ LOG_DEBUG << "loginUser"; - try { + try + { auto dbClientPtr = drogon::app().getDbClient(); Mapper mp(dbClientPtr); - if (!areFieldsValid(pUser)) { + if (!areFieldsValid(pUser)) + { Json::Value ret{}; ret["error"] = "missing fields"; auto resp = HttpResponse::newHttpJsonResponse(ret); @@ -74,7 +95,8 @@ void AuthController::loginUser(const HttpRequestPtr &req, std::function &mp) const { +bool AuthController::isUserAvailable(const User &user, Mapper &mp) const +{ auto criteria = Criteria(User::Cols::_username, CompareOperator::EQ, user.getValueOfUsername()); return mp.findFutureBy(criteria).get().empty(); } -bool AuthController::isPasswordValid(const std::string &text, const std::string &hash) const { +bool AuthController::isPasswordValid(const std::string &text, const std::string &hash) const +{ return BCrypt::validatePassword(text, hash); } -AuthController::UserWithToken::UserWithToken(const User &user) { +AuthController::UserWithToken::UserWithToken(const User &user) +{ auto *jwtPtr = drogon::app().getPlugin(); auto jwt = jwtPtr->init(); token = jwt.encode("user_id", user.getValueOfId()); username = user.getValueOfUsername(); } -Json::Value AuthController::UserWithToken::toJson() { +Json::Value AuthController::UserWithToken::toJson() +{ Json::Value ret{}; ret["username"] = username; ret["token"] = token; return ret; } + +void AuthController::deregisterUser( + const HttpRequestPtr &req, + std::function &&callback, + User && /*pUser*/) const // accept it, but ignore it. +{ + LOG_DEBUG << "deregisterUser"; + + const auto jsonPtr = req->getJsonObject(); + if (!jsonPtr || !jsonPtr->isMember("username") || !(*jsonPtr)["username"].isString()) + { + auto resp = HttpResponse::newHttpJsonResponse({{"error", "missing or invalid 'username'"}}); + resp->setStatusCode(HttpStatusCode::k400BadRequest); + return callback(resp); + } + + std::string username = (*jsonPtr)["username"].asString(); + if (username.empty() || username.size() > 128) + { + auto resp = HttpResponse::newHttpJsonResponse({{"error", "invalid 'username' length"}}); + resp->setStatusCode(HttpStatusCode::k400BadRequest); + return callback(resp); + } + + auto cbPtr = std::make_shared>( + std::move(callback)); + auto client = drogon::app().getDbClient(); + Mapper mp(client); + + mp.deleteBy( + Criteria(User::Cols::_username, CompareOperator::EQ, username), + [cbPtr](std::size_t count) + { + Json::Value body; + HttpStatusCode code; + if (count == 0) + { + body["error"] = "user not found"; + code = HttpStatusCode::k404NotFound; + } + else + { + body["message"] = "user deregistered successfully"; + code = HttpStatusCode::k200OK; + } + auto resp = HttpResponse::newHttpJsonResponse(body); + resp->setStatusCode(code); + (*cbPtr)(resp); + }, + [cbPtr](const DrogonDbException &e) + { + LOG_ERROR << e.base().what(); + auto resp = HttpResponse::newHttpJsonResponse({{"error", "database error"}}); + resp->setStatusCode(HttpStatusCode::k500InternalServerError); + (*cbPtr)(resp); + }); +} diff --git a/controllers/AuthController.h b/controllers/AuthController.h index f74c66f..861360a 100644 --- a/controllers/AuthController.h +++ b/controllers/AuthController.h @@ -8,26 +8,30 @@ using namespace drogon; using namespace drogon::orm; using namespace drogon_model::org_chart; -class AuthController : public drogon::HttpController { - public: - METHOD_LIST_BEGIN - ADD_METHOD_TO(AuthController::registerUser, "/auth/register", Post); - ADD_METHOD_TO(AuthController::loginUser, "/auth/login", Post); - METHOD_LIST_END +class AuthController : public drogon::HttpController +{ +public: + METHOD_LIST_BEGIN + ADD_METHOD_TO(AuthController::registerUser, "/auth/register", Post); + ADD_METHOD_TO(AuthController::deregisterUser, "/auth/deregister", Post); + ADD_METHOD_TO(AuthController::loginUser, "/auth/login", Post); + METHOD_LIST_END - void registerUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const; - void loginUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const; + void registerUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const; + void loginUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const; + void deregisterUser(const HttpRequestPtr &req, std::function &&callback, User &&pUser) const; - private: - struct UserWithToken { - std::string username; - std::string password; - std::string token; - explicit UserWithToken(const User &user); - Json::Value toJson(); - }; +private: + struct UserWithToken + { + std::string username; + std::string password; + std::string token; + explicit UserWithToken(const User &user); + Json::Value toJson(); + }; - bool areFieldsValid(const User &user) const; - bool isUserAvailable(const User &user, Mapper &mp) const; - bool isPasswordValid(const std::string &text, const std::string &hash) const; + bool areFieldsValid(const User &user) const; + bool isUserAvailable(const User &user, Mapper &mp) const; + bool isPasswordValid(const std::string &text, const std::string &hash) const; }; diff --git a/controllers/DepartmentsController.cc b/controllers/DepartmentsController.cc index 0dfeff0..a9e8c70 100644 --- a/controllers/DepartmentsController.cc +++ b/controllers/DepartmentsController.cc @@ -9,17 +9,30 @@ using namespace drogon::orm; using namespace drogon_model::org_chart; -namespace drogon { - template<> - inline Department fromRequest(const HttpRequest &req) { +namespace drogon +{ + template <> + inline Department fromRequest(const HttpRequest &req) + { auto jsonPtr = req.getJsonObject(); - auto json = *jsonPtr; - auto department = Department(json); - return department; + if (jsonPtr) + { + return Department(*jsonPtr); + } + + Json::Value json; + Json::Reader reader; + if (reader.parse(std::string(req.body()), json)) + { // Safe conversion! + return Department(json); + } + + return Department(Json::Value{}); } -} // namespace drogon +} -void DepartmentsController::get(const HttpRequestPtr &req, std::function &&callback) const { +void DepartmentsController::get(const HttpRequestPtr &req, std::function &&callback) const +{ LOG_DEBUG << "get"; auto offset = req->getOptionalParameter("offset").value_or(0); auto limit = req->getOptionalParameter("limit").value_or(25); @@ -30,42 +43,44 @@ void DepartmentsController::get(const HttpRequestPtr &req, std::function>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); Mapper mp(dbClientPtr); - mp.orderBy(sortField, sortOrderEnum).offset(offset).limit(limit).findAll( - [callbackPtr](const std::vector &departments) { + mp.orderBy(sortField, sortOrderEnum).offset(offset).limit(limit).findAll([callbackPtr](const std::vector &departments) + { Json::Value ret{}; for (auto d : departments) { ret.append(d.toJson()); } auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k200OK); - (*callbackPtr)(resp); - }, - [callbackPtr](const DrogonDbException &e) { + (*callbackPtr)(resp); }, [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); - (*callbackPtr)(resp); - }); + (*callbackPtr)(resp); }); } -void DepartmentsController::getOne(const HttpRequestPtr &req, std::function &&callback, int departmentId) const { - LOG_DEBUG << "getOne departmentId: "<< departmentId; +void DepartmentsController::getOne(const HttpRequestPtr &req, std::function &&callback, int departmentId) const +{ + LOG_DEBUG << "getOne departmentId: " << departmentId; auto callbackPtr = std::make_shared>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); Mapper mp(dbClientPtr); mp.findByPrimaryKey( departmentId, - [callbackPtr](const Department &department) { + [callbackPtr](const Department &department) + { Json::Value ret{}; ret = department.toJson(); auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k201Created); (*callbackPtr)(resp); }, - [callbackPtr](const DrogonDbException &e) { + [callbackPtr](const DrogonDbException &e) + { const drogon::orm::UnexpectedRows *s = dynamic_cast(&e.base()); - if(s) { + if (s) + { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k404NotFound); (*callbackPtr)(resp); @@ -75,10 +90,11 @@ void DepartmentsController::getOne(const HttpRequestPtr &req, std::functionsetStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - }); + }); } -void DepartmentsController::createOne(const HttpRequestPtr &req, std::function &&callback, Department &&pDepartment) const { +void DepartmentsController::createOne(const HttpRequestPtr &req, std::function &&callback, Department &&pDepartment) const +{ LOG_DEBUG << "createOne"; auto callbackPtr = std::make_shared>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); @@ -86,39 +102,47 @@ void DepartmentsController::createOne(const HttpRequestPtr &req, std::function mp(dbClientPtr); mp.insert( pDepartment, - [callbackPtr](const Department &department) { + [callbackPtr](const Department &department) + { Json::Value ret{}; ret = department.toJson(); auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k201Created); (*callbackPtr)(resp); }, - [callbackPtr](const DrogonDbException &e) { + [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - }); + }); } -void DepartmentsController::updateOne(const HttpRequestPtr &req, std::function &&callback, int departmentId, Department &&pDepartmentDetails) const { +void DepartmentsController::updateOne(const HttpRequestPtr &req, std::function &&callback, int departmentId, Department &&pDepartmentDetails) const +{ LOG_DEBUG << "updateOne departmentId: " << departmentId; auto dbClientPtr = drogon::app().getDbClient(); // blocking IO Mapper mp(dbClientPtr); Department department; - try { + try + { department = mp.findFutureByPrimaryKey(departmentId).get(); - } catch (const DrogonDbException & e) { + } + catch (const DrogonDbException &e) + { Json::Value ret{}; ret["error"] = "resource not found"; auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k404NotFound); callback(resp); + return; } - if (pDepartmentDetails.getName() != nullptr) { + if (pDepartmentDetails.getName() != nullptr) + { department.setName(pDepartmentDetails.getValueOfName()); } @@ -137,11 +161,11 @@ void DepartmentsController::updateOne(const HttpRequestPtr &req, std::functionsetStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - } - ); + }); } -void DepartmentsController::deleteOne(const HttpRequestPtr &req, std::function &&callback, int departmentId) const { +void DepartmentsController::deleteOne(const HttpRequestPtr &req, std::function &&callback, int departmentId) const +{ LOG_DEBUG << "deleteOne departmentId: "; auto callbackPtr = std::make_shared>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); @@ -149,39 +173,46 @@ void DepartmentsController::deleteOne(const HttpRequestPtr &req, std::function mp(dbClientPtr); mp.deleteBy( Criteria(Department::Cols::_id, CompareOperator::EQ, departmentId), - [callbackPtr](const std::size_t count) { + [callbackPtr](const std::size_t count) + { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(HttpStatusCode::k204NoContent); (*callbackPtr)(resp); }, - [callbackPtr](const DrogonDbException &e) { + [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - }); + }); } -void DepartmentsController::getDepartmentPersons(const HttpRequestPtr &req, std::function &&callback, int departmentId) const { - LOG_DEBUG << "getDepartmentPersons departmentId: "<< departmentId; +void DepartmentsController::getDepartmentPersons(const HttpRequestPtr &req, std::function &&callback, int departmentId) const +{ + LOG_DEBUG << "getDepartmentPersons departmentId: " << departmentId; auto callbackPtr = std::make_shared>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); // blocking IO Mapper mp(dbClientPtr); Department department; - try { + try + { department = mp.findFutureByPrimaryKey(departmentId).get(); - } catch (const DrogonDbException & e) { + } + catch (const DrogonDbException &e) + { Json::Value ret{}; ret["error"] = "resource not found"; auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k404NotFound); callback(resp); + return; } - department.getPersons(dbClientPtr, - [callbackPtr](const std::vector persons) { + department.getPersons(dbClientPtr, [callbackPtr](const std::vector persons) + { if (persons.empty()) { Json::Value ret{}; ret["error"] = "resource not found"; @@ -196,14 +227,12 @@ void DepartmentsController::getDepartmentPersons(const HttpRequestPtr &req, std: auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k200OK); (*callbackPtr)(resp); - } - }, - [callbackPtr](const DrogonDbException &e) { + } }, [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); Json::Value ret{}; ret["error"] = "database error"; auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k500InternalServerError); - (*callbackPtr)(resp); - }); + (*callbackPtr)(resp); }); } diff --git a/controllers/DepartmentsController.h b/controllers/DepartmentsController.h index 9fc7ef4..ca17307 100644 --- a/controllers/DepartmentsController.h +++ b/controllers/DepartmentsController.h @@ -6,21 +6,27 @@ using namespace drogon; using namespace drogon_model::org_chart; -class DepartmentsController : public drogon::HttpController { - public: - METHOD_LIST_BEGIN - ADD_METHOD_TO(DepartmentsController::get, "/departments", Get); - ADD_METHOD_TO(DepartmentsController::getOne, "/departments/{1}", Get); - ADD_METHOD_TO(DepartmentsController::createOne, "/departments", Post, "LoginFilter"); - ADD_METHOD_TO(DepartmentsController::updateOne, "/departments/{1}", Put, "LoginFilter"); - ADD_METHOD_TO(DepartmentsController::deleteOne, "/departments/{1}", Delete, "LoginFilter"); - ADD_METHOD_TO(DepartmentsController::getDepartmentPersons, "/departments/{1}/persons", Get, "LoginFilter"); - METHOD_LIST_END +class DepartmentsController : public drogon::HttpController +{ +public: + #ifndef FUZZING_BUILD - void get(const HttpRequestPtr& req, std::function &&callback) const; - void getOne(const HttpRequestPtr& req, std::function &&callback, int pDepartmentId) const; - void createOne(const HttpRequestPtr &req, std::function &&callback, Department &&pDepartment) const; - void updateOne(const HttpRequestPtr &req, std::function &&callback, int pDepartmentId, Department &&pDepartment) const; - void deleteOne(const HttpRequestPtr &req, std::function &&callback, int pDepartmentId) const; - void getDepartmentPersons(const HttpRequestPtr &req, std::function &&callback, int departmentId) const; + METHOD_LIST_BEGIN + METHOD_LIST_END + #else + METHOD_LIST_BEGIN + ADD_METHOD_TO(DepartmentsController::get, "/departments", Get, "LoginFilter"); + ADD_METHOD_TO(DepartmentsController::getOne, "/departments/{1}", Get, "LoginFilter"); + ADD_METHOD_TO(DepartmentsController::createOne, "/departments", Post, "LoginFilter"); + ADD_METHOD_TO(DepartmentsController::updateOne, "/departments/{1}", Put, "LoginFilter"); + ADD_METHOD_TO(DepartmentsController::deleteOne, "/departments/{1}", Delete, "LoginFilter"); + ADD_METHOD_TO(DepartmentsController::getDepartmentPersons, "/departments/{1}/persons", Get, "LoginFilter"); + METHOD_LIST_END + #endif + void get(const HttpRequestPtr &req, std::function &&callback) const; + void getOne(const HttpRequestPtr &req, std::function &&callback, int pDepartmentId) const; + void createOne(const HttpRequestPtr &req, std::function &&callback, Department &&pDepartment) const; + void updateOne(const HttpRequestPtr &req, std::function &&callback, int pDepartmentId, Department &&pDepartment) const; + void deleteOne(const HttpRequestPtr &req, std::function &&callback, int pDepartmentId) const; + void getDepartmentPersons(const HttpRequestPtr &req, std::function &&callback, int departmentId) const; }; diff --git a/controllers/JobsController.cc b/controllers/JobsController.cc index 88e65e4..c853de1 100644 --- a/controllers/JobsController.cc +++ b/controllers/JobsController.cc @@ -9,16 +9,38 @@ using namespace drogon::orm; using namespace drogon_model::org_chart; -namespace drogon { - template<> - inline Job fromRequest(const HttpRequest &req) { - auto json = req.getJsonObject(); - auto job = Job(*json); - return job; +JobsController::JobsController() +{ + dbClient_ = drogon::app().getDbClient(); +} + +JobsController::JobsController(std::shared_ptr dbClient) + : dbClient_(std::move(dbClient)) {} + +namespace drogon +{ + template <> + inline Job fromRequest(const HttpRequest &req) + { + auto jsonPtr = req.getJsonObject(); + if (jsonPtr) + { + return Job(*jsonPtr); + } + + Json::Value json; + Json::Reader reader; + if (reader.parse(std::string(req.body()), json)) + { + return Job(json); + } + + return Job(Json::Value{}); } } -void JobsController::get(const HttpRequestPtr &req, std::function &&callback) const { +void JobsController::get(const HttpRequestPtr &req, std::function &&callback) const +{ LOG_DEBUG << "get"; auto offset = req->getOptionalParameter("offset").value_or(0); auto limit = req->getOptionalParameter("limit").value_or(25); @@ -27,44 +49,45 @@ void JobsController::get(const HttpRequestPtr &req, std::function>(std::move(callback)); - auto dbClientPtr = drogon::app().getDbClient(); + auto dbClientPtr = dbClient_; Mapper mp(dbClientPtr); - mp.orderBy(sortField, sortOrderEnum).offset(offset).limit(limit).findAll( - [callbackPtr](const std::vector &jobs) { + mp.orderBy(sortField, sortOrderEnum).offset(offset).limit(limit).findAll([callbackPtr](const std::vector &jobs) + { Json::Value ret{}; for (auto j : jobs) { ret.append(j.toJson()); } auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k200OK); - (*callbackPtr)(resp); - }, - [callbackPtr](const DrogonDbException &e) { + (*callbackPtr)(resp); }, [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); - (*callbackPtr)(resp); - }); + (*callbackPtr)(resp); }); } -void JobsController::getOne(const HttpRequestPtr &req, std::function &&callback, int jobId) const { - LOG_DEBUG << "getOne jobId: "<< jobId; +void JobsController::getOne(const HttpRequestPtr &req, std::function &&callback, int jobId) const +{ + LOG_DEBUG << "getOne jobId: " << jobId; auto callbackPtr = std::make_shared>(std::move(callback)); - auto dbClientPtr = drogon::app().getDbClient(); - + auto dbClientPtr = dbClient_; Mapper mp(dbClientPtr); mp.findByPrimaryKey( jobId, - [callbackPtr](const Job &job) { + [callbackPtr](const Job &job) + { Json::Value ret{}; ret = job.toJson(); auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k201Created); (*callbackPtr)(resp); }, - [callbackPtr](const DrogonDbException &e) { + [callbackPtr](const DrogonDbException &e) + { const drogon::orm::UnexpectedRows *s = dynamic_cast(&e.base()); - if(s) { + if (s) + { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k404NotFound); (*callbackPtr)(resp); @@ -74,60 +97,68 @@ void JobsController::getOne(const HttpRequestPtr &req, std::functionsetStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - }); + }); } -void JobsController::createOne(const HttpRequestPtr &req, std::function &&callback, Job &&pJob) const { +void JobsController::createOne(const HttpRequestPtr &req, std::function &&callback, Job &&pJob) const +{ LOG_DEBUG << "createOne"; auto callbackPtr = std::make_shared>(std::move(callback)); - auto dbClientPtr = drogon::app().getDbClient(); - + auto dbClientPtr = dbClient_; Mapper mp(dbClientPtr); mp.insert( pJob, - [callbackPtr](const Job &job) { + [callbackPtr](const Job &job) + { Json::Value ret{}; ret = job.toJson(); auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k201Created); (*callbackPtr)(resp); }, - [callbackPtr](const DrogonDbException &e) { + [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - }); + }); } -void JobsController::updateOne(const HttpRequestPtr &req, std::function &&callback, int jobId, Job &&pJobDetails) const { +void JobsController::updateOne(const HttpRequestPtr &req, std::function &&callback, int jobId, Job &&pJobDetails) const +{ LOG_DEBUG << "updateOne jobId: " << jobId; auto jsonPtr = req->jsonObject(); - if (!jsonPtr) { - Json::Value ret{}; - ret["error"]="No json object is found in the request"; - auto resp = HttpResponse::newHttpResponse(); - resp->setStatusCode(HttpStatusCode::k400BadRequest); - callback(resp); - return; + if (!jsonPtr) + { + Json::Value ret{}; + ret["error"] = "No json object is found in the request"; + auto resp = HttpResponse::newHttpResponse(); + resp->setStatusCode(HttpStatusCode::k400BadRequest); + callback(resp); + return; } - auto dbClientPtr = drogon::app().getDbClient(); - + auto dbClientPtr = dbClient_; // blocking IO Mapper mp(dbClientPtr); Job job; - try { + try + { job = mp.findFutureByPrimaryKey(jobId).get(); - } catch (const DrogonDbException & e) { + } + catch (const DrogonDbException &e) + { Json::Value ret{}; ret["error"] = "resource not found"; auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k404NotFound); callback(resp); + return; } - if (pJobDetails.getTitle() != nullptr) { + if (pJobDetails.getTitle() != nullptr) + { job.setTitle(pJobDetails.getValueOfTitle()); } @@ -146,51 +177,56 @@ void JobsController::updateOne(const HttpRequestPtr &req, std::functionsetStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - } - ); + }); } -void JobsController::deleteOne(const HttpRequestPtr &req, std::function &&callback, int jobId) const { +void JobsController::deleteOne(const HttpRequestPtr &req, std::function &&callback, int jobId) const +{ LOG_DEBUG << "deleteOne jobId: "; auto callbackPtr = std::make_shared>(std::move(callback)); - auto dbClientPtr = drogon::app().getDbClient(); - + auto dbClientPtr = dbClient_; Mapper mp(dbClientPtr); mp.deleteBy( Criteria(Job::Cols::_id, CompareOperator::EQ, jobId), - [callbackPtr](const std::size_t count) { + [callbackPtr](const std::size_t count) + { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(HttpStatusCode::k204NoContent); (*callbackPtr)(resp); }, - [callbackPtr](const DrogonDbException &e) { + [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - }); + }); } -void JobsController::getJobPersons(const HttpRequestPtr &req, std::function &&callback, int jobId) const { - LOG_DEBUG << "getJobPersons jobId: "<< jobId; +void JobsController::getJobPersons(const HttpRequestPtr &req, std::function &&callback, int jobId) const +{ + LOG_DEBUG << "getJobPersons jobId: " << jobId; auto callbackPtr = std::make_shared>(std::move(callback)); - auto dbClientPtr = drogon::app().getDbClient(); - + auto dbClientPtr = dbClient_; // blocking IO Mapper mp(dbClientPtr); Job job; - try { + try + { job = mp.findFutureByPrimaryKey(jobId).get(); - } catch (const DrogonDbException & e) { + } + catch (const DrogonDbException &e) + { Json::Value ret{}; ret["error"] = "resource not found"; auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k404NotFound); callback(resp); + return; } - job.getPersons(dbClientPtr, - [callbackPtr](const std::vector persons) { + job.getPersons(dbClientPtr, [callbackPtr](const std::vector persons) + { if (persons.empty()) { Json::Value ret{}; ret["error"] = "resource not found"; @@ -205,14 +241,12 @@ void JobsController::getJobPersons(const HttpRequestPtr &req, std::functionsetStatusCode(HttpStatusCode::k200OK); (*callbackPtr)(resp); - } - }, - [callbackPtr](const DrogonDbException &e) { + } }, [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); Json::Value ret{}; ret["error"] = "database error"; auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k500InternalServerError); - (*callbackPtr)(resp); - }); + (*callbackPtr)(resp); }); } diff --git a/controllers/JobsController.h b/controllers/JobsController.h index 5116f3f..adebf14 100644 --- a/controllers/JobsController.h +++ b/controllers/JobsController.h @@ -2,25 +2,35 @@ #include #include "../models/Job.h" +#include +#include using namespace drogon; using namespace drogon_model::org_chart; +using namespace drogon::orm; -class JobsController : public drogon::HttpController { - public: - METHOD_LIST_BEGIN - ADD_METHOD_TO(JobsController::get, "/jobs", Get, "LoginFilter"); - ADD_METHOD_TO(JobsController::getOne, "/jobs/{1}", Get, "LoginFilter"); - ADD_METHOD_TO(JobsController::createOne, "/jobs", Post, "LoginFilter"); - ADD_METHOD_TO(JobsController::updateOne, "/jobs/{1}", Put, "LoginFilter"); - ADD_METHOD_TO(JobsController::deleteOne, "/jobs/{1}", Delete, "LoginFilter"); - ADD_METHOD_TO(JobsController::getJobPersons, "/jobs/{1}/persons", Get, "LoginFilter"); - METHOD_LIST_END +class JobsController : public drogon::HttpController +{ +public: + JobsController(); // Default constructor, defined in .cc + explicit JobsController(std::shared_ptr dbClient); - void get(const HttpRequestPtr& req, std::function &&callback) const; - void getOne(const HttpRequestPtr& req, std::function &&callback, int pJobId) const; - void createOne(const HttpRequestPtr &req, std::function &&callback, Job &&pJob) const; - void updateOne(const HttpRequestPtr &req, std::function &&callback, int pJobId, Job &&pJob) const; - void deleteOne(const HttpRequestPtr &req, std::function &&callback, int pJobId) const; - void getJobPersons(const HttpRequestPtr &req, std::function &&callback, int jobId) const; + METHOD_LIST_BEGIN + ADD_METHOD_TO(JobsController::get, "/jobs", Get, "LoginFilter"); + ADD_METHOD_TO(JobsController::getOne, "/jobs/{1}", Get, "LoginFilter"); + ADD_METHOD_TO(JobsController::createOne, "/jobs", Post, "LoginFilter"); + ADD_METHOD_TO(JobsController::updateOne, "/jobs/{1}", Put, "LoginFilter"); + ADD_METHOD_TO(JobsController::deleteOne, "/jobs/{1}", Delete, "LoginFilter"); + ADD_METHOD_TO(JobsController::getJobPersons, "/jobs/{1}/persons", Get, "LoginFilter"); + METHOD_LIST_END + + void get(const HttpRequestPtr &req, std::function &&callback) const; + void getOne(const HttpRequestPtr &req, std::function &&callback, int pJobId) const; + void createOne(const HttpRequestPtr &req, std::function &&callback, Job &&pJob) const; + void updateOne(const HttpRequestPtr &req, std::function &&callback, int pJobId, Job &&pJob) const; + void deleteOne(const HttpRequestPtr &req, std::function &&callback, int pJobId) const; + void getJobPersons(const HttpRequestPtr &req, std::function &&callback, int jobId) const; + +private: + std::shared_ptr dbClient_; }; diff --git a/controllers/PersonsController.cc b/controllers/PersonsController.cc index e8d3e89..36b24aa 100644 --- a/controllers/PersonsController.cc +++ b/controllers/PersonsController.cc @@ -8,20 +8,43 @@ using namespace drogon::orm; using namespace drogon_model::org_chart; -namespace drogon { - template<> - inline Person fromRequest(const HttpRequest &req) { +namespace drogon +{ + template <> + inline Person fromRequest(const HttpRequest &req) + { + // Try Content-Type: application/json first auto jsonPtr = req.getJsonObject(); - auto json = *jsonPtr; - if (json["department_id"]) json["department_id"] = std::stoi(json["department_id"].asString()); - if (json["manager_id"]) json["manager_id"] = std::stoi(json["manager_id"].asString()); - if (json["job_id"]) json["job_id"] = std::stoi(json["job_id"].asString()); - auto person = Person(json); - return person; + Json::Value json; + if (jsonPtr) + { + json = *jsonPtr; + } + else + { + // Fallback: parse raw body as JSON + Json::Reader reader; + if (!reader.parse(std::string(req.body()), json)) + { + // Parsing failed, return default Person + return Person(Json::Value{}); + } + } + + // Safely coerce string fields to int if present + if (json.isMember("department_id") && json["department_id"].isString()) + json["department_id"] = std::stoi(json["department_id"].asString()); + if (json.isMember("manager_id") && json["manager_id"].isString()) + json["manager_id"] = std::stoi(json["manager_id"].asString()); + if (json.isMember("job_id") && json["job_id"].isString()) + json["job_id"] = std::stoi(json["job_id"].asString()); + + return Person(json); } -} // namespace drogon +} // namespace drogon -void PersonsController::get(const HttpRequestPtr &req, std::function &&callback) const { +void PersonsController::get(const HttpRequestPtr &req, std::function &&callback) const +{ LOG_DEBUG << "get"; auto sort_field = req->getOptionalParameter("sort_field").value_or("id"); auto sort_order = req->getOptionalParameter("sort_order").value_or("asc"); @@ -37,48 +60,50 @@ void PersonsController::get(const HttpRequestPtr &req, std::function> [callbackPtr](const Result &result) - { - if (result.empty()) { - auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("resource not found")); - resp->setStatusCode(HttpStatusCode::k404NotFound); - (*callbackPtr)(resp); - return; - } - - Json::Value ret{}; - for (auto row : result) { - PersonInfo personInfo{row}; - PersonDetails personDetails{personInfo}; - ret.append(personDetails.toJson()); - } - - auto resp = HttpResponse::newHttpJsonResponse(ret); - resp->setStatusCode(HttpStatusCode::k200OK); - (*callbackPtr)(resp); - } - >> [callbackPtr](const DrogonDbException &e) - { - LOG_ERROR << e.base().what(); - auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); - resp->setStatusCode(HttpStatusCode::k500InternalServerError); - (*callbackPtr)(resp); - }; + << limit + << offset >> + [callbackPtr](const Result &result) + { + if (result.empty()) + { + auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("resource not found")); + resp->setStatusCode(HttpStatusCode::k404NotFound); + (*callbackPtr)(resp); + return; + } + + Json::Value ret{}; + for (auto row : result) + { + PersonInfo personInfo{row}; + PersonDetails personDetails{personInfo}; + ret.append(personDetails.toJson()); + } + + auto resp = HttpResponse::newHttpJsonResponse(ret); + resp->setStatusCode(HttpStatusCode::k200OK); + (*callbackPtr)(resp); + } >> [callbackPtr](const DrogonDbException &e) + { + LOG_ERROR << e.base().what(); + auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); + resp->setStatusCode(HttpStatusCode::k500InternalServerError); + (*callbackPtr)(resp); + }; } -void PersonsController::getOne(const HttpRequestPtr &req, std::function &&callback, int personId) const { - LOG_DEBUG << "getOne personId: "<< personId; +void PersonsController::getOne(const HttpRequestPtr &req, std::function &&callback, int personId) const +{ + LOG_DEBUG << "getOne personId: " << personId; auto callbackPtr = std::make_shared>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); @@ -89,94 +114,157 @@ void PersonsController::getOne(const HttpRequestPtr &req, std::function> [callbackPtr](const Result &result) - { - if (result.empty()) { - auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("resource not found")); - resp->setStatusCode(HttpStatusCode::k404NotFound); - (*callbackPtr)(resp); - return; - } - - auto row = result[0]; - PersonInfo personInfo{row}; - PersonDetails personDetails{personInfo}; - - Json::Value ret = personDetails.toJson(); - auto resp = HttpResponse::newHttpJsonResponse(ret); - resp->setStatusCode(HttpStatusCode::k200OK); - (*callbackPtr)(resp); - } - >> [callbackPtr](const DrogonDbException &e) - { - LOG_ERROR << e.base().what(); - auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); - resp->setStatusCode(HttpStatusCode::k500InternalServerError); - (*callbackPtr)(resp); - }; + << personId >> + [callbackPtr](const Result &result) + { + if (result.empty()) + { + auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("resource not found")); + resp->setStatusCode(HttpStatusCode::k404NotFound); + (*callbackPtr)(resp); + return; + } + + auto row = result[0]; + PersonInfo personInfo{row}; + PersonDetails personDetails{personInfo}; + + Json::Value ret = personDetails.toJson(); + auto resp = HttpResponse::newHttpJsonResponse(ret); + resp->setStatusCode(HttpStatusCode::k200OK); + (*callbackPtr)(resp); + } >> [callbackPtr](const DrogonDbException &e) + { + LOG_ERROR << e.base().what(); + auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); + resp->setStatusCode(HttpStatusCode::k500InternalServerError); + (*callbackPtr)(resp); + }; } -void PersonsController::createOne(const HttpRequestPtr &req, std::function &&callback, Person &&pPerson) const { +void PersonsController::createOne(const HttpRequestPtr &req, std::function &&callback, Person &&pPerson) const +{ LOG_DEBUG << "createOne"; auto callbackPtr = std::make_shared>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); + auto errResp = [&](const std::string &msg, int code = 400) + { + auto resp = HttpResponse::newHttpJsonResponse(makeErrResp(msg)); + resp->setStatusCode(static_cast(code)); + (*callbackPtr)(resp); + }; + + /* ---------- VALIDATE REQUIRED FIELDS ---------- */ + if (!pPerson.getLastName() || pPerson.getLastName()->empty()) + return errResp("last_name is compulsory"); + + if (!pPerson.getFirstName() || pPerson.getFirstName()->empty()) + return errResp("first_name is compulsory"); + + if (!pPerson.getHireDate()) + return errResp("hire_date is compulsory"); + + if (!pPerson.getDepartmentId()) + return errResp("department_id is compulsory"); + if (!rowExists(dbClientPtr, "department", pPerson.getValueOfDepartmentId())) + return errResp("department_id is invalid", 422); + + if (!pPerson.getJobId()) + return errResp("job_id is compulsory"); + if (!rowExists(dbClientPtr, "job", pPerson.getValueOfJobId())) + return errResp("job_id is invalid", 422); + + /* ---------- MANAGER (optional) ---------- */ + if (pPerson.getManagerId() && // provided + !rowExists(dbClientPtr, "person", pPerson.getValueOfManagerId())) + return errResp("manager_id is invalid", 422); + + if (personNameExists(dbClientPtr, + *pPerson.getFirstName(), + *pPerson.getLastName())) + return errResp("person with the same first_name and last_name already exists"); + Mapper mp(dbClientPtr); mp.insert( pPerson, - [callbackPtr](const Person &person) { + [callbackPtr](const Person &person) + { Json::Value ret{}; ret = person.toJson(); auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k201Created); (*callbackPtr)(resp); }, - [callbackPtr](const DrogonDbException &e) { + [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - }); + }); } -void PersonsController::updateOne(const HttpRequestPtr &req, std::function &&callback, int personId, Person &&pPerson) const { +void PersonsController::updateOne(const HttpRequestPtr &req, std::function &&callback, int personId, Person &&pPerson) const +{ LOG_DEBUG << "updateOne personId: " << personId; auto dbClientPtr = drogon::app().getDbClient(); // blocking IO Mapper mp(dbClientPtr); Person person; - try { + try + { person = mp.findFutureByPrimaryKey(personId).get(); - } catch (const DrogonDbException & e) { + } + catch (const DrogonDbException &e) + { auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("resource not found")); resp->setStatusCode(HttpStatusCode::k404NotFound); callback(resp); return; } - if (pPerson.getJobId() != nullptr) { - person.setJobId(pPerson.getValueOfJobId()); + auto callbackPtr = std::make_shared>(std::move(callback)); + + auto errResp = [&](const std::string &msg, int code = 400) + { + auto resp = HttpResponse::newHttpJsonResponse(makeErrResp(msg)); + resp->setStatusCode(static_cast(code)); + (*callbackPtr)(resp); + }; + + if (pPerson.getJobId() != nullptr) + { + if (!rowExists(dbClientPtr, "job", pPerson.getValueOfJobId())) + return errResp("job_id is invalid", 422); + person.setJobId(pPerson.getValueOfJobId()); } - if (pPerson.getManagerId() != nullptr) { - person.setManagerId(pPerson.getValueOfManagerId()); + if (pPerson.getManagerId() != nullptr) + { + if (!rowExists(dbClientPtr, "person", pPerson.getValueOfManagerId())) + return errResp("manager_id is invalid", 422); + person.setManagerId(pPerson.getValueOfManagerId()); } - if (pPerson.getDepartmentId() != nullptr) { - person.setDepartmentId(pPerson.getValueOfDepartmentId()); + if (pPerson.getDepartmentId() != nullptr) + { + if (!rowExists(dbClientPtr, "department", pPerson.getValueOfDepartmentId())) + return errResp("department_id is invalid", 422); + person.setDepartmentId(pPerson.getValueOfDepartmentId()); } - if (pPerson.getFirstName() != nullptr) { - person.setFirstName(pPerson.getValueOfFirstName()); + if (pPerson.getFirstName() != nullptr) + { + person.setFirstName(pPerson.getValueOfFirstName()); } - if (pPerson.getLastName() != nullptr) { - person.setLastName(pPerson.getValueOfLastName()); + if (pPerson.getLastName() != nullptr) + { + person.setLastName(pPerson.getValueOfLastName()); } - auto callbackPtr = std::make_shared>(std::move(callback)); mp.update( person, [callbackPtr](const std::size_t count) @@ -191,11 +279,11 @@ void PersonsController::updateOne(const HttpRequestPtr &req, std::functionsetStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - } - ); + }); } -void PersonsController::deleteOne(const HttpRequestPtr &req, std::function &&callback, int personId) const { +void PersonsController::deleteOne(const HttpRequestPtr &req, std::function &&callback, int personId) const +{ LOG_DEBUG << "deleteOne personId: "; auto callbackPtr = std::make_shared>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); @@ -203,37 +291,44 @@ void PersonsController::deleteOne(const HttpRequestPtr &req, std::function mp(dbClientPtr); mp.deleteBy( Criteria(Person::Cols::_id, CompareOperator::EQ, personId), - [callbackPtr](const std::size_t count) { + [callbackPtr](const std::size_t count) + { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(HttpStatusCode::k204NoContent); (*callbackPtr)(resp); }, - [callbackPtr](const DrogonDbException &e) { + [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); (*callbackPtr)(resp); - }); + }); } -void PersonsController::getDirectReports(const HttpRequestPtr &req, std::function &&callback, int personId) const { - LOG_DEBUG << "getDirectReports personId: "<< personId; +void PersonsController::getDirectReports(const HttpRequestPtr &req, std::function &&callback, int personId) const +{ + LOG_DEBUG << "getDirectReports personId: " << personId; auto callbackPtr = std::make_shared>(std::move(callback)); auto dbClientPtr = drogon::app().getDbClient(); // blocking IO Mapper mp(dbClientPtr); Person department; - try { + try + { department = mp.findFutureByPrimaryKey(personId).get(); - } catch (const DrogonDbException & e) { + } + catch (const DrogonDbException &e) + { auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("resource not found")); resp->setStatusCode(HttpStatusCode::k404NotFound); callback(resp); + return; } - department.getPersons(dbClientPtr, - [callbackPtr](const std::vector persons) { + department.getPersons(dbClientPtr, [callbackPtr](const std::vector persons) + { if (persons.empty()) { auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("resource not found")); resp->setStatusCode(HttpStatusCode::k404NotFound); @@ -246,17 +341,16 @@ void PersonsController::getDirectReports(const HttpRequestPtr &req, std::functio auto resp = HttpResponse::newHttpJsonResponse(ret); resp->setStatusCode(HttpStatusCode::k200OK); (*callbackPtr)(resp); - } - }, - [callbackPtr](const DrogonDbException &e) { + } }, [callbackPtr](const DrogonDbException &e) + { LOG_ERROR << e.base().what(); auto resp = HttpResponse::newHttpJsonResponse(makeErrResp("database error")); resp->setStatusCode(HttpStatusCode::k500InternalServerError); - (*callbackPtr)(resp); - }); + (*callbackPtr)(resp); }); } -PersonsController::PersonDetails::PersonDetails(const PersonInfo &personInfo) { +PersonsController::PersonDetails::PersonDetails(const PersonInfo &personInfo) +{ id = personInfo.getValueOfId(); first_name = personInfo.getValueOfFirstName(); last_name = personInfo.getValueOfLastName(); @@ -275,7 +369,8 @@ PersonsController::PersonDetails::PersonDetails(const PersonInfo &personInfo) { this->job = jobJson; } -auto PersonsController::PersonDetails::toJson() -> Json::Value { +auto PersonsController::PersonDetails::toJson() -> Json::Value +{ Json::Value ret{}; ret["id"] = id; ret["first_name"] = first_name; diff --git a/controllers/PersonsController.h b/controllers/PersonsController.h index c5a4cb5..772e32b 100644 --- a/controllers/PersonsController.h +++ b/controllers/PersonsController.h @@ -8,35 +8,37 @@ using namespace drogon; using namespace drogon_model::org_chart; -class PersonsController : public drogon::HttpController { - public: - METHOD_LIST_BEGIN - ADD_METHOD_TO(PersonsController::get, "/persons", Get); - ADD_METHOD_TO(PersonsController::getOne, "/persons/{1}", Get); - ADD_METHOD_TO(PersonsController::createOne, "/persons", Post); - ADD_METHOD_TO(PersonsController::updateOne, "/persons/{1}", Put); - ADD_METHOD_TO(PersonsController::deleteOne, "/persons/{1}", Delete); - ADD_METHOD_TO(PersonsController::getDirectReports, "/persons/{1}/reports", Get); - METHOD_LIST_END +class PersonsController : public drogon::HttpController +{ +public: + METHOD_LIST_BEGIN + ADD_METHOD_TO(PersonsController::get, "/persons", Get, "LoginFilter"); + ADD_METHOD_TO(PersonsController::getOne, "/persons/{1}", Get, "LoginFilter"); + ADD_METHOD_TO(PersonsController::createOne, "/persons", Post, "LoginFilter"); + ADD_METHOD_TO(PersonsController::updateOne, "/persons/{1}", Put, "LoginFilter"); + ADD_METHOD_TO(PersonsController::deleteOne, "/persons/{1}", Delete, "LoginFilter"); + ADD_METHOD_TO(PersonsController::getDirectReports, "/persons/{1}/reports", Get, "LoginFilter"); + METHOD_LIST_END - void get(const HttpRequestPtr& req, std::function &&callback) const; - void getOne(const HttpRequestPtr& req, std::function &&callback, int pPersonId) const; - void createOne(const HttpRequestPtr &req, std::function &&callback, Person &&pPerson) const; - void updateOne(const HttpRequestPtr &req, std::function &&callback, int pPersonId, Person &&pPerson) const; - void deleteOne(const HttpRequestPtr &req, std::function &&callback, int pPersonId) const; - void getDirectReports(const HttpRequestPtr &req, std::function &&callback, int pPersonId) const; + void get(const HttpRequestPtr &req, std::function &&callback) const; + void getOne(const HttpRequestPtr &req, std::function &&callback, int pPersonId) const; + void createOne(const HttpRequestPtr &req, std::function &&callback, Person &&pPerson) const; + void updateOne(const HttpRequestPtr &req, std::function &&callback, int pPersonId, Person &&pPerson) const; + void deleteOne(const HttpRequestPtr &req, std::function &&callback, int pPersonId) const; + void getDirectReports(const HttpRequestPtr &req, std::function &&callback, int pPersonId) const; - private: - struct PersonDetails { - int id; - std::string first_name; - std::string last_name; - trantor::Date hire_date; - Json::Value manager; - Json::Value department; - Json::Value job; - PersonDetails() {} - explicit PersonDetails(const PersonInfo &personInfo); - Json::Value toJson(); - }; +private: + struct PersonDetails + { + int id; + std::string first_name; + std::string last_name; + trantor::Date hire_date; + Json::Value manager; + Json::Value department; + Json::Value job; + PersonDetails() {} + explicit PersonDetails(const PersonInfo &personInfo); + Json::Value toJson(); + }; }; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..366129e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,50 @@ +version: '3.8' + +services: + db: + image: mysql:8.3 # or any 8.x you prefer + container_name: mysql + restart: always + environment: + # root account (use strong pw in prod) + MYSQL_ROOT_PASSWORD: password + # an app-level DB + user the app can connect with + MYSQL_DATABASE: org_chart + MYSQL_USER: org + MYSQL_PASSWORD: password + ports: + - "3307:3306" # host:container + volumes: + - mysql_data:/var/lib/mysql + # every *.sql in this dir runs exactly once at first boot + - ./scripts:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-ppassword"] + interval: 5s + timeout: 3s + retries: 5 + + app: + build: . + container_name: drogon_app + ports: + - "3000:3000" + depends_on: + db: + condition: service_healthy + volumes: + - .:/app + - /app/build + environment: + - DB_HOST=db + - DB_PORT=3306 + - DB_USER=org + - DB_PASSWORD=password + - DB_NAME=org_chart + # optional hint for many frameworks/ORMS + - DB_DRIVER=mysql + working_dir: /app/build + command: ["./org_chart"] + +volumes: + mysql_data: diff --git a/harness.cpp b/harness.cpp new file mode 100644 index 0000000..e80cec1 --- /dev/null +++ b/harness.cpp @@ -0,0 +1,65 @@ +#include "controllers/DepartmentsController.h" +#include +#include +#include +#include + +// This is the main entry point for the fuzzer +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + static bool is_initialized = []() -> bool { + try { + drogon::app().disableSession(); + drogon::app().setLogLevel(trantor::Logger::kError); + drogon::app().loadConfigFile("config.json"); + std::cout << "Drogon configured for fuzzing with in-memory SQLite database." << std::endl; + + } catch (const std::exception& e) { + // If this fails, the harness is misconfigured. + std::cerr << "!!! HARNESS INITIALIZATION FAILED: " << e.what() << std::endl; + abort(); + } + return true; + }(); + // --- End of Initialization --- + + if (Size < 1) { + return 0; + } + + DepartmentsController controller; + uint8_t selector = Data[0]; + const uint8_t* payload = Data + 1; + size_t payload_size = Size - 1; + + try { + switch (selector % 2) { + case 0: { // Fuzz createOne + if (payload_size < 2) break; + Json::Value json_body; + Json::Reader reader; + std::string input_string(reinterpret_cast(payload), payload_size); + if (reader.parse(input_string, json_body)) { + Department department_to_create(json_body); + auto req = drogon::HttpRequest::newHttpJsonRequest(json_body); + auto cb = [](const drogon::HttpResponsePtr &){}; + controller.createOne(req, std::move(cb), std::move(department_to_create)); + } + break; + } + case 1: { // Fuzz getOne + if (payload_size == 0) break; + int departmentId = 0; + size_t bytes_to_copy = std::min(payload_size, sizeof(int)); + memcpy(&departmentId, payload, bytes_to_copy); + auto req = drogon::HttpRequest::newHttpRequest(); + auto cb = [](const drogon::HttpResponsePtr &){}; + controller.getOne(req, std::move(cb), departmentId); + break; + } + } + } catch (const std::exception &e) { + // Correctly catch exceptions from business logic. + } + + return 0; +} diff --git a/models/Department.cc b/models/Department.cc index 27e3eb0..83513e8 100644 --- a/models/Department.cc +++ b/models/Department.cc @@ -21,8 +21,8 @@ const bool Department::hasPrimaryKey = true; const std::string Department::tableName = "department"; const std::vector Department::metaData_={ -{"id","int32_t","integer",4,1,1,1}, -{"name","std::string","character varying",50,0,0,1} +{"id","uint64_t","bigint unsigned",8,1,1,1}, +{"name","std::string","varchar(50)",50,0,0,1} }; const std::string &Department::getColumnName(size_t index) noexcept(false) { @@ -35,7 +35,7 @@ Department::Department(const Row &r, const ssize_t indexOffset) noexcept { if(!r["id"].isNull()) { - id_=std::make_shared(r["id"].as()); + id_=std::make_shared(r["id"].as()); } if(!r["name"].isNull()) { @@ -54,7 +54,7 @@ Department::Department(const Row &r, const ssize_t indexOffset) noexcept index = offset + 0; if(!r[index].isNull()) { - id_=std::make_shared(r[index].as()); + id_=std::make_shared(r[index].as()); } index = offset + 1; if(!r[index].isNull()) @@ -77,7 +77,7 @@ Department::Department(const Json::Value &pJson, const std::vector dirtyFlag_[0] = true; if(!pJson[pMasqueradingVector[0]].isNull()) { - id_=std::make_shared((int32_t)pJson[pMasqueradingVector[0]].asInt64()); + id_=std::make_shared((uint64_t)pJson[pMasqueradingVector[0]].asUInt64()); } } if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) @@ -97,7 +97,7 @@ Department::Department(const Json::Value &pJson) noexcept(false) dirtyFlag_[0]=true; if(!pJson["id"].isNull()) { - id_=std::make_shared((int32_t)pJson["id"].asInt64()); + id_=std::make_shared((uint64_t)pJson["id"].asUInt64()); } } if(pJson.isMember("name")) @@ -122,7 +122,7 @@ void Department::updateByMasqueradedJson(const Json::Value &pJson, { if(!pJson[pMasqueradingVector[0]].isNull()) { - id_=std::make_shared((int32_t)pJson[pMasqueradingVector[0]].asInt64()); + id_=std::make_shared((uint64_t)pJson[pMasqueradingVector[0]].asUInt64()); } } if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) @@ -141,7 +141,7 @@ void Department::updateByJson(const Json::Value &pJson) noexcept(false) { if(!pJson["id"].isNull()) { - id_=std::make_shared((int32_t)pJson["id"].asInt64()); + id_=std::make_shared((uint64_t)pJson["id"].asUInt64()); } } if(pJson.isMember("name")) @@ -154,20 +154,20 @@ void Department::updateByJson(const Json::Value &pJson) noexcept(false) } } -const int32_t &Department::getValueOfId() const noexcept +const uint64_t &Department::getValueOfId() const noexcept { - const static int32_t defaultValue = int32_t(); + static const uint64_t defaultValue = uint64_t(); if(id_) return *id_; return defaultValue; } -const std::shared_ptr &Department::getId() const noexcept +const std::shared_ptr &Department::getId() const noexcept { return id_; } -void Department::setId(const int32_t &pId) noexcept +void Department::setId(const uint64_t &pId) noexcept { - id_ = std::make_shared(pId); + id_ = std::make_shared(pId); dirtyFlag_[0] = true; } const typename Department::PrimaryKeyType & Department::getPrimaryKey() const @@ -178,7 +178,7 @@ const typename Department::PrimaryKeyType & Department::getPrimaryKey() const const std::string &Department::getValueOfName() const noexcept { - const static std::string defaultValue = std::string(); + static const std::string defaultValue = std::string(); if(name_) return *name_; return defaultValue; @@ -200,6 +200,7 @@ void Department::setName(std::string &&pName) noexcept void Department::updateId(const uint64_t id) { + id_ = std::make_shared(id); } const std::vector &Department::insertColumns() noexcept @@ -254,7 +255,7 @@ Json::Value Department::toJson() const Json::Value ret; if(getId()) { - ret["id"]=getValueOfId(); + ret["id"]=(Json::UInt64)getValueOfId(); } else { @@ -271,6 +272,11 @@ Json::Value Department::toJson() const return ret; } +std::string Department::toString() const +{ + return toJson().toStyledString(); +} + Json::Value Department::toMasqueradedJson( const std::vector &pMasqueradingVector) const { @@ -281,7 +287,7 @@ Json::Value Department::toMasqueradedJson( { if(getId()) { - ret[pMasqueradingVector[0]]=getValueOfId(); + ret[pMasqueradingVector[0]]=(Json::UInt64)getValueOfId(); } else { @@ -304,7 +310,7 @@ Json::Value Department::toMasqueradedJson( LOG_ERROR << "Masquerade failed"; if(getId()) { - ret["id"]=getValueOfId(); + ret["id"]=(Json::UInt64)getValueOfId(); } else { @@ -450,7 +456,7 @@ bool Department::validJsonOfField(size_t index, err="The automatic primary key cannot be set"; return false; } - if(!pJson.isInt()) + if(!pJson.isUInt64()) { err="Type error in the "+fieldName+" field"; return false; @@ -467,8 +473,7 @@ bool Department::validJsonOfField(size_t index, err="Type error in the "+fieldName+" field"; return false; } - // asString().length() creates a string object, is there any better way to validate the length? - if(pJson.isString() && pJson.asString().length() > 50) + if(pJson.isString() && std::strlen(pJson.asCString()) > 50) { err="String length exceeds limit for the " + fieldName + @@ -480,15 +485,32 @@ bool Department::validJsonOfField(size_t index, default: err="Internal error in the server"; return false; - break; } return true; } +std::vector Department::getPersons(const DbClientPtr &clientPtr) const { + static const std::string sql = "select * from person where department_id = ?"; + Result r(nullptr); + { + auto binder = *clientPtr << sql; + binder << *id_ << Mode::Blocking >> + [&r](const Result &result) { r = result; }; + binder.exec(); + } + std::vector ret; + ret.reserve(r.size()); + for (auto const &row : r) + { + ret.emplace_back(Person(row)); + } + return ret; +} + void Department::getPersons(const DbClientPtr &clientPtr, const std::function)> &rcb, const ExceptionCallback &ecb) const { - const static std::string sql = "select * from person where department_id = $1"; + static const std::string sql = "select * from person where department_id = ?"; *clientPtr << sql << *id_ >> [rcb = std::move(rcb)](const Result &r){ diff --git a/models/Department.h b/models/Department.h index 9e82611..9ce6535 100644 --- a/models/Department.h +++ b/models/Department.h @@ -11,6 +11,7 @@ #include #include #include +#include #ifdef __cpp_impl_coroutine #include #endif @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -47,11 +49,11 @@ class Department static const std::string _name; }; - const static int primaryKeyNumber; - const static std::string tableName; - const static bool hasPrimaryKey; - const static std::string primaryKeyName; - using PrimaryKeyType = int32_t; + static const int primaryKeyNumber; + static const std::string tableName; + static const bool hasPrimaryKey; + static const std::string primaryKeyName; + using PrimaryKeyType = uint64_t; const PrimaryKeyType &getPrimaryKey() const; /** @@ -98,11 +100,11 @@ class Department /** For column id */ ///Get the value of the column id, returns the default value if the column is null - const int32_t &getValueOfId() const noexcept; + const uint64_t &getValueOfId() const noexcept; ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getId() const noexcept; + const std::shared_ptr &getId() const noexcept; ///Set the value of the column id - void setId(const int32_t &pId) noexcept; + void setId(const uint64_t &pId) noexcept; /** For column name */ ///Get the value of the column name, returns the default value if the column is null @@ -118,13 +120,19 @@ class Department static const std::string &getColumnName(size_t index) noexcept(false); Json::Value toJson() const; + std::string toString() const; Json::Value toMasqueradedJson(const std::vector &pMasqueradingVector) const; /// Relationship interfaces + std::vector getPersons(const drogon::orm::DbClientPtr &clientPtr) const; void getPersons(const drogon::orm::DbClientPtr &clientPtr, const std::function)> &rcb, const drogon::orm::ExceptionCallback &ecb) const; private: friend drogon::orm::Mapper; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; #ifdef __cpp_impl_coroutine friend drogon::orm::CoroMapper; #endif @@ -134,7 +142,7 @@ class Department void updateArgs(drogon::orm::internal::SqlBinder &binder) const; ///For mysql or sqlite3 void updateId(const uint64_t id); - std::shared_ptr id_; + std::shared_ptr id_; std::shared_ptr name_; struct MetaData { @@ -151,13 +159,13 @@ class Department public: static const std::string &sqlForFindingByPrimaryKey() { - static const std::string sql="select * from " + tableName + " where id = $1"; + static const std::string sql="select * from " + tableName + " where id = ?"; return sql; } static const std::string &sqlForDeletingByPrimaryKey() { - static const std::string sql="delete from " + tableName + " where id = $1"; + static const std::string sql="delete from " + tableName + " where id = ?"; return sql; } std::string sqlForInserting(bool &needSelection) const @@ -181,27 +189,17 @@ class Department else sql += ") values ("; - int placeholder=1; - char placeholderStr[64]; - size_t n=0; sql +="default,"; if(dirtyFlag_[1]) { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); + sql.append("?,"); + } if(parametersCount > 0) { sql.resize(sql.length() - 1); } - if(needSelection) - { - sql.append(") returning *"); - } - else - { - sql.append(1, ')'); - } + sql.append(1, ')'); LOG_TRACE << sql; return sql; } diff --git a/models/Job.cc b/models/Job.cc index ac059fa..98a318f 100644 --- a/models/Job.cc +++ b/models/Job.cc @@ -21,8 +21,8 @@ const bool Job::hasPrimaryKey = true; const std::string Job::tableName = "job"; const std::vector Job::metaData_={ -{"id","int32_t","integer",4,1,1,1}, -{"title","std::string","character varying",50,0,0,1} +{"id","uint64_t","bigint unsigned",8,1,1,1}, +{"title","std::string","varchar(50)",50,0,0,1} }; const std::string &Job::getColumnName(size_t index) noexcept(false) { @@ -35,7 +35,7 @@ Job::Job(const Row &r, const ssize_t indexOffset) noexcept { if(!r["id"].isNull()) { - id_=std::make_shared(r["id"].as()); + id_=std::make_shared(r["id"].as()); } if(!r["title"].isNull()) { @@ -54,7 +54,7 @@ Job::Job(const Row &r, const ssize_t indexOffset) noexcept index = offset + 0; if(!r[index].isNull()) { - id_=std::make_shared(r[index].as()); + id_=std::make_shared(r[index].as()); } index = offset + 1; if(!r[index].isNull()) @@ -77,7 +77,7 @@ Job::Job(const Json::Value &pJson, const std::vector &pMasquerading dirtyFlag_[0] = true; if(!pJson[pMasqueradingVector[0]].isNull()) { - id_=std::make_shared((int32_t)pJson[pMasqueradingVector[0]].asInt64()); + id_=std::make_shared((uint64_t)pJson[pMasqueradingVector[0]].asUInt64()); } } if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) @@ -97,7 +97,7 @@ Job::Job(const Json::Value &pJson) noexcept(false) dirtyFlag_[0]=true; if(!pJson["id"].isNull()) { - id_=std::make_shared((int32_t)pJson["id"].asInt64()); + id_=std::make_shared((uint64_t)pJson["id"].asUInt64()); } } if(pJson.isMember("title")) @@ -122,7 +122,7 @@ void Job::updateByMasqueradedJson(const Json::Value &pJson, { if(!pJson[pMasqueradingVector[0]].isNull()) { - id_=std::make_shared((int32_t)pJson[pMasqueradingVector[0]].asInt64()); + id_=std::make_shared((uint64_t)pJson[pMasqueradingVector[0]].asUInt64()); } } if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) @@ -141,7 +141,7 @@ void Job::updateByJson(const Json::Value &pJson) noexcept(false) { if(!pJson["id"].isNull()) { - id_=std::make_shared((int32_t)pJson["id"].asInt64()); + id_=std::make_shared((uint64_t)pJson["id"].asUInt64()); } } if(pJson.isMember("title")) @@ -154,20 +154,20 @@ void Job::updateByJson(const Json::Value &pJson) noexcept(false) } } -const int32_t &Job::getValueOfId() const noexcept +const uint64_t &Job::getValueOfId() const noexcept { - const static int32_t defaultValue = int32_t(); + static const uint64_t defaultValue = uint64_t(); if(id_) return *id_; return defaultValue; } -const std::shared_ptr &Job::getId() const noexcept +const std::shared_ptr &Job::getId() const noexcept { return id_; } -void Job::setId(const int32_t &pId) noexcept +void Job::setId(const uint64_t &pId) noexcept { - id_ = std::make_shared(pId); + id_ = std::make_shared(pId); dirtyFlag_[0] = true; } const typename Job::PrimaryKeyType & Job::getPrimaryKey() const @@ -178,7 +178,7 @@ const typename Job::PrimaryKeyType & Job::getPrimaryKey() const const std::string &Job::getValueOfTitle() const noexcept { - const static std::string defaultValue = std::string(); + static const std::string defaultValue = std::string(); if(title_) return *title_; return defaultValue; @@ -200,6 +200,7 @@ void Job::setTitle(std::string &&pTitle) noexcept void Job::updateId(const uint64_t id) { + id_ = std::make_shared(id); } const std::vector &Job::insertColumns() noexcept @@ -254,7 +255,7 @@ Json::Value Job::toJson() const Json::Value ret; if(getId()) { - ret["id"]=getValueOfId(); + ret["id"]=(Json::UInt64)getValueOfId(); } else { @@ -271,6 +272,11 @@ Json::Value Job::toJson() const return ret; } +std::string Job::toString() const +{ + return toJson().toStyledString(); +} + Json::Value Job::toMasqueradedJson( const std::vector &pMasqueradingVector) const { @@ -281,7 +287,7 @@ Json::Value Job::toMasqueradedJson( { if(getId()) { - ret[pMasqueradingVector[0]]=getValueOfId(); + ret[pMasqueradingVector[0]]=(Json::UInt64)getValueOfId(); } else { @@ -304,7 +310,7 @@ Json::Value Job::toMasqueradedJson( LOG_ERROR << "Masquerade failed"; if(getId()) { - ret["id"]=getValueOfId(); + ret["id"]=(Json::UInt64)getValueOfId(); } else { @@ -450,7 +456,7 @@ bool Job::validJsonOfField(size_t index, err="The automatic primary key cannot be set"; return false; } - if(!pJson.isInt()) + if(!pJson.isUInt64()) { err="Type error in the "+fieldName+" field"; return false; @@ -467,8 +473,7 @@ bool Job::validJsonOfField(size_t index, err="Type error in the "+fieldName+" field"; return false; } - // asString().length() creates a string object, is there any better way to validate the length? - if(pJson.isString() && pJson.asString().length() > 50) + if(pJson.isString() && std::strlen(pJson.asCString()) > 50) { err="String length exceeds limit for the " + fieldName + @@ -480,15 +485,32 @@ bool Job::validJsonOfField(size_t index, default: err="Internal error in the server"; return false; - break; } return true; } +std::vector Job::getPersons(const DbClientPtr &clientPtr) const { + static const std::string sql = "select * from person where job_id = ?"; + Result r(nullptr); + { + auto binder = *clientPtr << sql; + binder << *id_ << Mode::Blocking >> + [&r](const Result &result) { r = result; }; + binder.exec(); + } + std::vector ret; + ret.reserve(r.size()); + for (auto const &row : r) + { + ret.emplace_back(Person(row)); + } + return ret; +} + void Job::getPersons(const DbClientPtr &clientPtr, const std::function)> &rcb, const ExceptionCallback &ecb) const { - const static std::string sql = "select * from person where job_id = $1"; + static const std::string sql = "select * from person where job_id = ?"; *clientPtr << sql << *id_ >> [rcb = std::move(rcb)](const Result &r){ diff --git a/models/Job.h b/models/Job.h index 821ca99..756595e 100644 --- a/models/Job.h +++ b/models/Job.h @@ -11,6 +11,7 @@ #include #include #include +#include #ifdef __cpp_impl_coroutine #include #endif @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -47,11 +49,11 @@ class Job static const std::string _title; }; - const static int primaryKeyNumber; - const static std::string tableName; - const static bool hasPrimaryKey; - const static std::string primaryKeyName; - using PrimaryKeyType = int32_t; + static const int primaryKeyNumber; + static const std::string tableName; + static const bool hasPrimaryKey; + static const std::string primaryKeyName; + using PrimaryKeyType = uint64_t; const PrimaryKeyType &getPrimaryKey() const; /** @@ -98,11 +100,11 @@ class Job /** For column id */ ///Get the value of the column id, returns the default value if the column is null - const int32_t &getValueOfId() const noexcept; + const uint64_t &getValueOfId() const noexcept; ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getId() const noexcept; + const std::shared_ptr &getId() const noexcept; ///Set the value of the column id - void setId(const int32_t &pId) noexcept; + void setId(const uint64_t &pId) noexcept; /** For column title */ ///Get the value of the column title, returns the default value if the column is null @@ -118,13 +120,19 @@ class Job static const std::string &getColumnName(size_t index) noexcept(false); Json::Value toJson() const; + std::string toString() const; Json::Value toMasqueradedJson(const std::vector &pMasqueradingVector) const; /// Relationship interfaces + std::vector getPersons(const drogon::orm::DbClientPtr &clientPtr) const; void getPersons(const drogon::orm::DbClientPtr &clientPtr, const std::function)> &rcb, const drogon::orm::ExceptionCallback &ecb) const; private: friend drogon::orm::Mapper; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; #ifdef __cpp_impl_coroutine friend drogon::orm::CoroMapper; #endif @@ -134,7 +142,7 @@ class Job void updateArgs(drogon::orm::internal::SqlBinder &binder) const; ///For mysql or sqlite3 void updateId(const uint64_t id); - std::shared_ptr id_; + std::shared_ptr id_; std::shared_ptr title_; struct MetaData { @@ -151,13 +159,13 @@ class Job public: static const std::string &sqlForFindingByPrimaryKey() { - static const std::string sql="select * from " + tableName + " where id = $1"; + static const std::string sql="select * from " + tableName + " where id = ?"; return sql; } static const std::string &sqlForDeletingByPrimaryKey() { - static const std::string sql="delete from " + tableName + " where id = $1"; + static const std::string sql="delete from " + tableName + " where id = ?"; return sql; } std::string sqlForInserting(bool &needSelection) const @@ -181,27 +189,17 @@ class Job else sql += ") values ("; - int placeholder=1; - char placeholderStr[64]; - size_t n=0; sql +="default,"; if(dirtyFlag_[1]) { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); + sql.append("?,"); + } if(parametersCount > 0) { sql.resize(sql.length() - 1); } - if(needSelection) - { - sql.append(") returning *"); - } - else - { - sql.append(1, ')'); - } + sql.append(1, ')'); LOG_TRACE << sql; return sql; } diff --git a/models/Person.cc b/models/Person.cc index 2b73111..56cab54 100644 --- a/models/Person.cc +++ b/models/Person.cc @@ -27,12 +27,12 @@ const bool Person::hasPrimaryKey = true; const std::string Person::tableName = "person"; const std::vector Person::metaData_={ -{"id","int32_t","integer",4,1,1,1}, -{"job_id","int32_t","integer",4,0,0,1}, -{"department_id","int32_t","integer",4,0,0,1}, -{"manager_id","int32_t","integer",4,0,0,1}, -{"first_name","std::string","character varying",50,0,0,1}, -{"last_name","std::string","character varying",50,0,0,1}, +{"id","uint64_t","bigint unsigned",8,1,1,1}, +{"job_id","uint64_t","bigint unsigned",8,0,0,1}, +{"department_id","uint64_t","bigint unsigned",8,0,0,0}, +{"manager_id","uint64_t","bigint unsigned",8,0,0,0}, +{"first_name","std::string","varchar(50)",50,0,0,1}, +{"last_name","std::string","varchar(50)",50,0,0,1}, {"hire_date","::trantor::Date","date",0,0,0,1} }; const std::string &Person::getColumnName(size_t index) noexcept(false) @@ -46,19 +46,19 @@ Person::Person(const Row &r, const ssize_t indexOffset) noexcept { if(!r["id"].isNull()) { - id_=std::make_shared(r["id"].as()); + id_=std::make_shared(r["id"].as()); } if(!r["job_id"].isNull()) { - jobId_=std::make_shared(r["job_id"].as()); + jobId_=std::make_shared(r["job_id"].as()); } if(!r["department_id"].isNull()) { - departmentId_=std::make_shared(r["department_id"].as()); + departmentId_=std::make_shared(r["department_id"].as()); } if(!r["manager_id"].isNull()) { - managerId_=std::make_shared(r["manager_id"].as()); + managerId_=std::make_shared(r["manager_id"].as()); } if(!r["first_name"].isNull()) { @@ -90,22 +90,22 @@ Person::Person(const Row &r, const ssize_t indexOffset) noexcept index = offset + 0; if(!r[index].isNull()) { - id_=std::make_shared(r[index].as()); + id_=std::make_shared(r[index].as()); } index = offset + 1; if(!r[index].isNull()) { - jobId_=std::make_shared(r[index].as()); + jobId_=std::make_shared(r[index].as()); } index = offset + 2; if(!r[index].isNull()) { - departmentId_=std::make_shared(r[index].as()); + departmentId_=std::make_shared(r[index].as()); } index = offset + 3; if(!r[index].isNull()) { - managerId_=std::make_shared(r[index].as()); + managerId_=std::make_shared(r[index].as()); } index = offset + 4; if(!r[index].isNull()) @@ -143,7 +143,7 @@ Person::Person(const Json::Value &pJson, const std::vector &pMasque dirtyFlag_[0] = true; if(!pJson[pMasqueradingVector[0]].isNull()) { - id_=std::make_shared((int32_t)pJson[pMasqueradingVector[0]].asInt64()); + id_=std::make_shared((uint64_t)pJson[pMasqueradingVector[0]].asUInt64()); } } if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) @@ -151,7 +151,7 @@ Person::Person(const Json::Value &pJson, const std::vector &pMasque dirtyFlag_[1] = true; if(!pJson[pMasqueradingVector[1]].isNull()) { - jobId_=std::make_shared((int32_t)pJson[pMasqueradingVector[1]].asInt64()); + jobId_=std::make_shared((uint64_t)pJson[pMasqueradingVector[1]].asUInt64()); } } if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2])) @@ -159,7 +159,7 @@ Person::Person(const Json::Value &pJson, const std::vector &pMasque dirtyFlag_[2] = true; if(!pJson[pMasqueradingVector[2]].isNull()) { - departmentId_=std::make_shared((int32_t)pJson[pMasqueradingVector[2]].asInt64()); + departmentId_=std::make_shared((uint64_t)pJson[pMasqueradingVector[2]].asUInt64()); } } if(!pMasqueradingVector[3].empty() && pJson.isMember(pMasqueradingVector[3])) @@ -167,7 +167,7 @@ Person::Person(const Json::Value &pJson, const std::vector &pMasque dirtyFlag_[3] = true; if(!pJson[pMasqueradingVector[3]].isNull()) { - managerId_=std::make_shared((int32_t)pJson[pMasqueradingVector[3]].asInt64()); + managerId_=std::make_shared((uint64_t)pJson[pMasqueradingVector[3]].asUInt64()); } } if(!pMasqueradingVector[4].empty() && pJson.isMember(pMasqueradingVector[4])) @@ -208,7 +208,7 @@ Person::Person(const Json::Value &pJson) noexcept(false) dirtyFlag_[0]=true; if(!pJson["id"].isNull()) { - id_=std::make_shared((int32_t)pJson["id"].asInt64()); + id_=std::make_shared((uint64_t)pJson["id"].asUInt64()); } } if(pJson.isMember("job_id")) @@ -216,7 +216,7 @@ Person::Person(const Json::Value &pJson) noexcept(false) dirtyFlag_[1]=true; if(!pJson["job_id"].isNull()) { - jobId_=std::make_shared((int32_t)pJson["job_id"].asInt64()); + jobId_=std::make_shared((uint64_t)pJson["job_id"].asUInt64()); } } if(pJson.isMember("department_id")) @@ -224,7 +224,7 @@ Person::Person(const Json::Value &pJson) noexcept(false) dirtyFlag_[2]=true; if(!pJson["department_id"].isNull()) { - departmentId_=std::make_shared((int32_t)pJson["department_id"].asInt64()); + departmentId_=std::make_shared((uint64_t)pJson["department_id"].asUInt64()); } } if(pJson.isMember("manager_id")) @@ -232,7 +232,7 @@ Person::Person(const Json::Value &pJson) noexcept(false) dirtyFlag_[3]=true; if(!pJson["manager_id"].isNull()) { - managerId_=std::make_shared((int32_t)pJson["manager_id"].asInt64()); + managerId_=std::make_shared((uint64_t)pJson["manager_id"].asUInt64()); } } if(pJson.isMember("first_name")) @@ -278,7 +278,7 @@ void Person::updateByMasqueradedJson(const Json::Value &pJson, { if(!pJson[pMasqueradingVector[0]].isNull()) { - id_=std::make_shared((int32_t)pJson[pMasqueradingVector[0]].asInt64()); + id_=std::make_shared((uint64_t)pJson[pMasqueradingVector[0]].asUInt64()); } } if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) @@ -286,7 +286,7 @@ void Person::updateByMasqueradedJson(const Json::Value &pJson, dirtyFlag_[1] = true; if(!pJson[pMasqueradingVector[1]].isNull()) { - jobId_=std::make_shared((int32_t)pJson[pMasqueradingVector[1]].asInt64()); + jobId_=std::make_shared((uint64_t)pJson[pMasqueradingVector[1]].asUInt64()); } } if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2])) @@ -294,7 +294,7 @@ void Person::updateByMasqueradedJson(const Json::Value &pJson, dirtyFlag_[2] = true; if(!pJson[pMasqueradingVector[2]].isNull()) { - departmentId_=std::make_shared((int32_t)pJson[pMasqueradingVector[2]].asInt64()); + departmentId_=std::make_shared((uint64_t)pJson[pMasqueradingVector[2]].asUInt64()); } } if(!pMasqueradingVector[3].empty() && pJson.isMember(pMasqueradingVector[3])) @@ -302,7 +302,7 @@ void Person::updateByMasqueradedJson(const Json::Value &pJson, dirtyFlag_[3] = true; if(!pJson[pMasqueradingVector[3]].isNull()) { - managerId_=std::make_shared((int32_t)pJson[pMasqueradingVector[3]].asInt64()); + managerId_=std::make_shared((uint64_t)pJson[pMasqueradingVector[3]].asUInt64()); } } if(!pMasqueradingVector[4].empty() && pJson.isMember(pMasqueradingVector[4])) @@ -342,7 +342,7 @@ void Person::updateByJson(const Json::Value &pJson) noexcept(false) { if(!pJson["id"].isNull()) { - id_=std::make_shared((int32_t)pJson["id"].asInt64()); + id_=std::make_shared((uint64_t)pJson["id"].asUInt64()); } } if(pJson.isMember("job_id")) @@ -350,7 +350,7 @@ void Person::updateByJson(const Json::Value &pJson) noexcept(false) dirtyFlag_[1] = true; if(!pJson["job_id"].isNull()) { - jobId_=std::make_shared((int32_t)pJson["job_id"].asInt64()); + jobId_=std::make_shared((uint64_t)pJson["job_id"].asUInt64()); } } if(pJson.isMember("department_id")) @@ -358,7 +358,7 @@ void Person::updateByJson(const Json::Value &pJson) noexcept(false) dirtyFlag_[2] = true; if(!pJson["department_id"].isNull()) { - departmentId_=std::make_shared((int32_t)pJson["department_id"].asInt64()); + departmentId_=std::make_shared((uint64_t)pJson["department_id"].asUInt64()); } } if(pJson.isMember("manager_id")) @@ -366,7 +366,7 @@ void Person::updateByJson(const Json::Value &pJson) noexcept(false) dirtyFlag_[3] = true; if(!pJson["manager_id"].isNull()) { - managerId_=std::make_shared((int32_t)pJson["manager_id"].asInt64()); + managerId_=std::make_shared((uint64_t)pJson["manager_id"].asUInt64()); } } if(pJson.isMember("first_name")) @@ -400,20 +400,20 @@ void Person::updateByJson(const Json::Value &pJson) noexcept(false) } } -const int32_t &Person::getValueOfId() const noexcept +const uint64_t &Person::getValueOfId() const noexcept { - const static int32_t defaultValue = int32_t(); + static const uint64_t defaultValue = uint64_t(); if(id_) return *id_; return defaultValue; } -const std::shared_ptr &Person::getId() const noexcept +const std::shared_ptr &Person::getId() const noexcept { return id_; } -void Person::setId(const int32_t &pId) noexcept +void Person::setId(const uint64_t &pId) noexcept { - id_ = std::make_shared(pId); + id_ = std::make_shared(pId); dirtyFlag_[0] = true; } const typename Person::PrimaryKeyType & Person::getPrimaryKey() const @@ -422,60 +422,70 @@ const typename Person::PrimaryKeyType & Person::getPrimaryKey() const return *id_; } -const int32_t &Person::getValueOfJobId() const noexcept +const uint64_t &Person::getValueOfJobId() const noexcept { - const static int32_t defaultValue = int32_t(); + static const uint64_t defaultValue = uint64_t(); if(jobId_) return *jobId_; return defaultValue; } -const std::shared_ptr &Person::getJobId() const noexcept +const std::shared_ptr &Person::getJobId() const noexcept { return jobId_; } -void Person::setJobId(const int32_t &pJobId) noexcept +void Person::setJobId(const uint64_t &pJobId) noexcept { - jobId_ = std::make_shared(pJobId); + jobId_ = std::make_shared(pJobId); dirtyFlag_[1] = true; } -const int32_t &Person::getValueOfDepartmentId() const noexcept +const uint64_t &Person::getValueOfDepartmentId() const noexcept { - const static int32_t defaultValue = int32_t(); + static const uint64_t defaultValue = uint64_t(); if(departmentId_) return *departmentId_; return defaultValue; } -const std::shared_ptr &Person::getDepartmentId() const noexcept +const std::shared_ptr &Person::getDepartmentId() const noexcept { return departmentId_; } -void Person::setDepartmentId(const int32_t &pDepartmentId) noexcept +void Person::setDepartmentId(const uint64_t &pDepartmentId) noexcept { - departmentId_ = std::make_shared(pDepartmentId); + departmentId_ = std::make_shared(pDepartmentId); + dirtyFlag_[2] = true; +} +void Person::setDepartmentIdToNull() noexcept +{ + departmentId_.reset(); dirtyFlag_[2] = true; } -const int32_t &Person::getValueOfManagerId() const noexcept +const uint64_t &Person::getValueOfManagerId() const noexcept { - const static int32_t defaultValue = int32_t(); + static const uint64_t defaultValue = uint64_t(); if(managerId_) return *managerId_; return defaultValue; } -const std::shared_ptr &Person::getManagerId() const noexcept +const std::shared_ptr &Person::getManagerId() const noexcept { return managerId_; } -void Person::setManagerId(const int32_t &pManagerId) noexcept +void Person::setManagerId(const uint64_t &pManagerId) noexcept { - managerId_ = std::make_shared(pManagerId); + managerId_ = std::make_shared(pManagerId); + dirtyFlag_[3] = true; +} +void Person::setManagerIdToNull() noexcept +{ + managerId_.reset(); dirtyFlag_[3] = true; } const std::string &Person::getValueOfFirstName() const noexcept { - const static std::string defaultValue = std::string(); + static const std::string defaultValue = std::string(); if(firstName_) return *firstName_; return defaultValue; @@ -497,7 +507,7 @@ void Person::setFirstName(std::string &&pFirstName) noexcept const std::string &Person::getValueOfLastName() const noexcept { - const static std::string defaultValue = std::string(); + static const std::string defaultValue = std::string(); if(lastName_) return *lastName_; return defaultValue; @@ -519,7 +529,7 @@ void Person::setLastName(std::string &&pLastName) noexcept const ::trantor::Date &Person::getValueOfHireDate() const noexcept { - const static ::trantor::Date defaultValue = ::trantor::Date(); + static const ::trantor::Date defaultValue = ::trantor::Date(); if(hireDate_) return *hireDate_; return defaultValue; @@ -536,6 +546,7 @@ void Person::setHireDate(const ::trantor::Date &pHireDate) noexcept void Person::updateId(const uint64_t id) { + id_ = std::make_shared(id); } const std::vector &Person::insertColumns() noexcept @@ -725,7 +736,7 @@ Json::Value Person::toJson() const Json::Value ret; if(getId()) { - ret["id"]=getValueOfId(); + ret["id"]=(Json::UInt64)getValueOfId(); } else { @@ -733,7 +744,7 @@ Json::Value Person::toJson() const } if(getJobId()) { - ret["job_id"]=getValueOfJobId(); + ret["job_id"]=(Json::UInt64)getValueOfJobId(); } else { @@ -741,7 +752,7 @@ Json::Value Person::toJson() const } if(getDepartmentId()) { - ret["department_id"]=getValueOfDepartmentId(); + ret["department_id"]=(Json::UInt64)getValueOfDepartmentId(); } else { @@ -749,7 +760,7 @@ Json::Value Person::toJson() const } if(getManagerId()) { - ret["manager_id"]=getValueOfManagerId(); + ret["manager_id"]=(Json::UInt64)getValueOfManagerId(); } else { @@ -782,6 +793,11 @@ Json::Value Person::toJson() const return ret; } +std::string Person::toString() const +{ + return toJson().toStyledString(); +} + Json::Value Person::toMasqueradedJson( const std::vector &pMasqueradingVector) const { @@ -792,7 +808,7 @@ Json::Value Person::toMasqueradedJson( { if(getId()) { - ret[pMasqueradingVector[0]]=getValueOfId(); + ret[pMasqueradingVector[0]]=(Json::UInt64)getValueOfId(); } else { @@ -803,7 +819,7 @@ Json::Value Person::toMasqueradedJson( { if(getJobId()) { - ret[pMasqueradingVector[1]]=getValueOfJobId(); + ret[pMasqueradingVector[1]]=(Json::UInt64)getValueOfJobId(); } else { @@ -814,7 +830,7 @@ Json::Value Person::toMasqueradedJson( { if(getDepartmentId()) { - ret[pMasqueradingVector[2]]=getValueOfDepartmentId(); + ret[pMasqueradingVector[2]]=(Json::UInt64)getValueOfDepartmentId(); } else { @@ -825,7 +841,7 @@ Json::Value Person::toMasqueradedJson( { if(getManagerId()) { - ret[pMasqueradingVector[3]]=getValueOfManagerId(); + ret[pMasqueradingVector[3]]=(Json::UInt64)getValueOfManagerId(); } else { @@ -870,7 +886,7 @@ Json::Value Person::toMasqueradedJson( LOG_ERROR << "Masquerade failed"; if(getId()) { - ret["id"]=getValueOfId(); + ret["id"]=(Json::UInt64)getValueOfId(); } else { @@ -878,7 +894,7 @@ Json::Value Person::toMasqueradedJson( } if(getJobId()) { - ret["job_id"]=getValueOfJobId(); + ret["job_id"]=(Json::UInt64)getValueOfJobId(); } else { @@ -886,7 +902,7 @@ Json::Value Person::toMasqueradedJson( } if(getDepartmentId()) { - ret["department_id"]=getValueOfDepartmentId(); + ret["department_id"]=(Json::UInt64)getValueOfDepartmentId(); } else { @@ -894,7 +910,7 @@ Json::Value Person::toMasqueradedJson( } if(getManagerId()) { - ret["manager_id"]=getValueOfManagerId(); + ret["manager_id"]=(Json::UInt64)getValueOfManagerId(); } else { @@ -949,21 +965,11 @@ bool Person::validateJsonForCreation(const Json::Value &pJson, std::string &err) if(!validJsonOfField(2, "department_id", pJson["department_id"], err, true)) return false; } - else - { - err="The department_id column cannot be null"; - return false; - } if(pJson.isMember("manager_id")) { if(!validJsonOfField(3, "manager_id", pJson["manager_id"], err, true)) return false; } - else - { - err="The manager_id column cannot be null"; - return false; - } if(pJson.isMember("first_name")) { if(!validJsonOfField(4, "first_name", pJson["first_name"], err, true)) @@ -1034,11 +1040,6 @@ bool Person::validateMasqueradedJsonForCreation(const Json::Value &pJson, if(!validJsonOfField(2, pMasqueradingVector[2], pJson[pMasqueradingVector[2]], err, true)) return false; } - else - { - err="The " + pMasqueradingVector[2] + " column cannot be null"; - return false; - } } if(!pMasqueradingVector[3].empty()) { @@ -1047,11 +1048,6 @@ bool Person::validateMasqueradedJsonForCreation(const Json::Value &pJson, if(!validJsonOfField(3, pMasqueradingVector[3], pJson[pMasqueradingVector[3]], err, true)) return false; } - else - { - err="The " + pMasqueradingVector[3] + " column cannot be null"; - return false; - } } if(!pMasqueradingVector[4].empty()) { @@ -1221,7 +1217,7 @@ bool Person::validJsonOfField(size_t index, err="The automatic primary key cannot be set"; return false; } - if(!pJson.isInt()) + if(!pJson.isUInt64()) { err="Type error in the "+fieldName+" field"; return false; @@ -1233,7 +1229,7 @@ bool Person::validJsonOfField(size_t index, err="The " + fieldName + " column cannot be null"; return false; } - if(!pJson.isInt()) + if(!pJson.isUInt64()) { err="Type error in the "+fieldName+" field"; return false; @@ -1242,10 +1238,9 @@ bool Person::validJsonOfField(size_t index, case 2: if(pJson.isNull()) { - err="The " + fieldName + " column cannot be null"; - return false; + return true; } - if(!pJson.isInt()) + if(!pJson.isUInt64()) { err="Type error in the "+fieldName+" field"; return false; @@ -1254,10 +1249,9 @@ bool Person::validJsonOfField(size_t index, case 3: if(pJson.isNull()) { - err="The " + fieldName + " column cannot be null"; - return false; + return true; } - if(!pJson.isInt()) + if(!pJson.isUInt64()) { err="Type error in the "+fieldName+" field"; return false; @@ -1274,8 +1268,7 @@ bool Person::validJsonOfField(size_t index, err="Type error in the "+fieldName+" field"; return false; } - // asString().length() creates a string object, is there any better way to validate the length? - if(pJson.isString() && pJson.asString().length() > 50) + if(pJson.isString() && std::strlen(pJson.asCString()) > 50) { err="String length exceeds limit for the " + fieldName + @@ -1295,8 +1288,7 @@ bool Person::validJsonOfField(size_t index, err="Type error in the "+fieldName+" field"; return false; } - // asString().length() creates a string object, is there any better way to validate the length? - if(pJson.isString() && pJson.asString().length() > 50) + if(pJson.isString() && std::strlen(pJson.asCString()) > 50) { err="String length exceeds limit for the " + fieldName + @@ -1320,15 +1312,34 @@ bool Person::validJsonOfField(size_t index, default: err="Internal error in the server"; return false; - break; } return true; } +Department Person::getDepartment(const DbClientPtr &clientPtr) const { + static const std::string sql = "select * from department where id = ?"; + Result r(nullptr); + { + auto binder = *clientPtr << sql; + binder << *departmentId_ << Mode::Blocking >> + [&r](const Result &result) { r = result; }; + binder.exec(); + } + if (r.size() == 0) + { + throw UnexpectedRows("0 rows found"); + } + else if (r.size() > 1) + { + throw UnexpectedRows("Found more than one row"); + } + return Department(r[0]); +} + void Person::getDepartment(const DbClientPtr &clientPtr, const std::function &rcb, const ExceptionCallback &ecb) const { - const static std::string sql = "select * from department where id = $1"; + static const std::string sql = "select * from department where id = ?"; *clientPtr << sql << *departmentId_ >> [rcb = std::move(rcb), ecb](const Result &r){ @@ -1347,11 +1358,31 @@ void Person::getDepartment(const DbClientPtr &clientPtr, } >> ecb; } +Job Person::getJob(const DbClientPtr &clientPtr) const { + static const std::string sql = "select * from job where id = ?"; + Result r(nullptr); + { + auto binder = *clientPtr << sql; + binder << *jobId_ << Mode::Blocking >> + [&r](const Result &result) { r = result; }; + binder.exec(); + } + if (r.size() == 0) + { + throw UnexpectedRows("0 rows found"); + } + else if (r.size() > 1) + { + throw UnexpectedRows("Found more than one row"); + } + return Job(r[0]); +} + void Person::getJob(const DbClientPtr &clientPtr, const std::function &rcb, const ExceptionCallback &ecb) const { - const static std::string sql = "select * from job where id = $1"; + static const std::string sql = "select * from job where id = ?"; *clientPtr << sql << *jobId_ >> [rcb = std::move(rcb), ecb](const Result &r){ @@ -1370,11 +1401,29 @@ void Person::getJob(const DbClientPtr &clientPtr, } >> ecb; } +std::vector Person::getPersons(const DbClientPtr &clientPtr) const { + static const std::string sql = "select * from person where manager_id = ?"; + Result r(nullptr); + { + auto binder = *clientPtr << sql; + binder << *id_ << Mode::Blocking >> + [&r](const Result &result) { r = result; }; + binder.exec(); + } + std::vector ret; + ret.reserve(r.size()); + for (auto const &row : r) + { + ret.emplace_back(Person(row)); + } + return ret; +} + void Person::getPersons(const DbClientPtr &clientPtr, const std::function)> &rcb, const ExceptionCallback &ecb) const { - const static std::string sql = "select * from person where manager_id = $1"; + static const std::string sql = "select * from person where manager_id = ?"; *clientPtr << sql << *id_ >> [rcb = std::move(rcb)](const Result &r){ diff --git a/models/Person.h b/models/Person.h index 1efcc03..1e6421d 100644 --- a/models/Person.h +++ b/models/Person.h @@ -11,6 +11,7 @@ #include #include #include +#include #ifdef __cpp_impl_coroutine #include #endif @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -54,11 +56,11 @@ class Person static const std::string _hire_date; }; - const static int primaryKeyNumber; - const static std::string tableName; - const static bool hasPrimaryKey; - const static std::string primaryKeyName; - using PrimaryKeyType = int32_t; + static const int primaryKeyNumber; + static const std::string tableName; + static const bool hasPrimaryKey; + static const std::string primaryKeyName; + using PrimaryKeyType = uint64_t; const PrimaryKeyType &getPrimaryKey() const; /** @@ -105,35 +107,37 @@ class Person /** For column id */ ///Get the value of the column id, returns the default value if the column is null - const int32_t &getValueOfId() const noexcept; + const uint64_t &getValueOfId() const noexcept; ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getId() const noexcept; + const std::shared_ptr &getId() const noexcept; ///Set the value of the column id - void setId(const int32_t &pId) noexcept; + void setId(const uint64_t &pId) noexcept; /** For column job_id */ ///Get the value of the column job_id, returns the default value if the column is null - const int32_t &getValueOfJobId() const noexcept; + const uint64_t &getValueOfJobId() const noexcept; ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getJobId() const noexcept; + const std::shared_ptr &getJobId() const noexcept; ///Set the value of the column job_id - void setJobId(const int32_t &pJobId) noexcept; + void setJobId(const uint64_t &pJobId) noexcept; /** For column department_id */ ///Get the value of the column department_id, returns the default value if the column is null - const int32_t &getValueOfDepartmentId() const noexcept; + const uint64_t &getValueOfDepartmentId() const noexcept; ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getDepartmentId() const noexcept; + const std::shared_ptr &getDepartmentId() const noexcept; ///Set the value of the column department_id - void setDepartmentId(const int32_t &pDepartmentId) noexcept; + void setDepartmentId(const uint64_t &pDepartmentId) noexcept; + void setDepartmentIdToNull() noexcept; /** For column manager_id */ ///Get the value of the column manager_id, returns the default value if the column is null - const int32_t &getValueOfManagerId() const noexcept; + const uint64_t &getValueOfManagerId() const noexcept; ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getManagerId() const noexcept; + const std::shared_ptr &getManagerId() const noexcept; ///Set the value of the column manager_id - void setManagerId(const int32_t &pManagerId) noexcept; + void setManagerId(const uint64_t &pManagerId) noexcept; + void setManagerIdToNull() noexcept; /** For column first_name */ ///Get the value of the column first_name, returns the default value if the column is null @@ -166,19 +170,27 @@ class Person static const std::string &getColumnName(size_t index) noexcept(false); Json::Value toJson() const; + std::string toString() const; Json::Value toMasqueradedJson(const std::vector &pMasqueradingVector) const; /// Relationship interfaces + Department getDepartment(const drogon::orm::DbClientPtr &clientPtr) const; void getDepartment(const drogon::orm::DbClientPtr &clientPtr, const std::function &rcb, const drogon::orm::ExceptionCallback &ecb) const; + Job getJob(const drogon::orm::DbClientPtr &clientPtr) const; void getJob(const drogon::orm::DbClientPtr &clientPtr, const std::function &rcb, const drogon::orm::ExceptionCallback &ecb) const; + std::vector getPersons(const drogon::orm::DbClientPtr &clientPtr) const; void getPersons(const drogon::orm::DbClientPtr &clientPtr, const std::function)> &rcb, const drogon::orm::ExceptionCallback &ecb) const; private: friend drogon::orm::Mapper; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; #ifdef __cpp_impl_coroutine friend drogon::orm::CoroMapper; #endif @@ -188,10 +200,10 @@ class Person void updateArgs(drogon::orm::internal::SqlBinder &binder) const; ///For mysql or sqlite3 void updateId(const uint64_t id); - std::shared_ptr id_; - std::shared_ptr jobId_; - std::shared_ptr departmentId_; - std::shared_ptr managerId_; + std::shared_ptr id_; + std::shared_ptr jobId_; + std::shared_ptr departmentId_; + std::shared_ptr managerId_; std::shared_ptr firstName_; std::shared_ptr lastName_; std::shared_ptr<::trantor::Date> hireDate_; @@ -210,13 +222,13 @@ class Person public: static const std::string &sqlForFindingByPrimaryKey() { - static const std::string sql="select * from " + tableName + " where id = $1"; + static const std::string sql="select * from " + tableName + " where id = ?"; return sql; } static const std::string &sqlForDeletingByPrimaryKey() { - static const std::string sql="delete from " + tableName + " where id = $1"; + static const std::string sql="delete from " + tableName + " where id = ?"; return sql; } std::string sqlForInserting(bool &needSelection) const @@ -265,52 +277,42 @@ class Person else sql += ") values ("; - int placeholder=1; - char placeholderStr[64]; - size_t n=0; sql +="default,"; if(dirtyFlag_[1]) { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); + sql.append("?,"); + } if(dirtyFlag_[2]) { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); + sql.append("?,"); + } if(dirtyFlag_[3]) { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); + sql.append("?,"); + } if(dirtyFlag_[4]) { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); + sql.append("?,"); + } if(dirtyFlag_[5]) { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); + sql.append("?,"); + } if(dirtyFlag_[6]) { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); + sql.append("?,"); + } if(parametersCount > 0) { sql.resize(sql.length() - 1); } - if(needSelection) - { - sql.append(") returning *"); - } - else - { - sql.append(1, ')'); - } + sql.append(1, ')'); LOG_TRACE << sql; return sql; } diff --git a/models/PersonInfo.h b/models/PersonInfo.h index 966c626..720df30 100644 --- a/models/PersonInfo.h +++ b/models/PersonInfo.h @@ -1,3 +1,9 @@ +/** + * PersonInfo.h (MySQL-ready) + * NOTE: This class is a read-only view/DTO; it has no SQL-generation code + * so only the preamble comment is adjusted. Everything else remains intact. + */ + #pragma once #include #include @@ -16,98 +22,67 @@ namespace drogon { -namespace orm -{ -class DbClient; -using DbClientPtr = std::shared_ptr; -} + namespace orm + { + class DbClient; + using DbClientPtr = std::shared_ptr; + } } namespace drogon_model { -namespace org_chart -{ - -class PersonInfo -{ - public: - - explicit PersonInfo(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept; + namespace org_chart + { + class PersonInfo + { + public: + explicit PersonInfo(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept; + PersonInfo() = default; - PersonInfo() = default; + /* column accessors (generated) */ + const int32_t &getValueOfId() const noexcept; + const std::shared_ptr &getId() const noexcept; - /** For column id */ - ///Get the value of the column id, returns the default value if the column is null - const int32_t &getValueOfId() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getId() const noexcept; + const int32_t &getValueOfJobId() const noexcept; + const std::shared_ptr &getJobId() const noexcept; - /** For column job_id */ - ///Get the value of the column job_id, returns the default value if the column is null - const int32_t &getValueOfJobId() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getJobId() const noexcept; + const std::string &getValueOfJobTitle() const noexcept; + const std::shared_ptr &getJobTitle() const noexcept; - /** For column job_title */ - ///Get the value of the column job_title, returns the default value if the column is null - const std::string &getValueOfJobTitle() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getJobTitle() const noexcept; + const int32_t &getValueOfDepartmentId() const noexcept; + const std::shared_ptr &getDepartmentId() const noexcept; - /** For column department_id */ - ///Get the value of the column department_id, returns the default value if the column is null - const int32_t &getValueOfDepartmentId() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getDepartmentId() const noexcept; + const std::string &getValueOfDepartmentName() const noexcept; + const std::shared_ptr &getDepartmentName() const noexcept; - /** For column department_name */ - ///Get the value of the column department_name, returns the default value if the column is null - const std::string &getValueOfDepartmentName() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getDepartmentName() const noexcept; + const int32_t &getValueOfManagerId() const noexcept; + const std::shared_ptr &getManagerId() const noexcept; - /** For column manager_id */ - ///Get the value of the column manager_id, returns the default value if the column is null - const int32_t &getValueOfManagerId() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getManagerId() const noexcept; + const std::string &getValueOfManagerFullName() const noexcept; + const std::shared_ptr &getManagerFullName() const noexcept; - /** For column manager_full_name */ - ///Get the value of the column first_name, returns the default value if the column is null - const std::string &getValueOfManagerFullName() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getManagerFullName() const noexcept; + const std::string &getValueOfFirstName() const noexcept; + const std::shared_ptr &getFirstName() const noexcept; - /** For column first_name */ - ///Get the value of the column first_name, returns the default value if the column is null - const std::string &getValueOfFirstName() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getFirstName() const noexcept; + const std::string &getValueOfLastName() const noexcept; + const std::shared_ptr &getLastName() const noexcept; - /** For column last_name */ - ///Get the value of the column last_name, returns the default value if the column is null - const std::string &getValueOfLastName() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getLastName() const noexcept; + const ::trantor::Date &getValueOfHireDate() const noexcept; + const std::shared_ptr<::trantor::Date> &getHireDate() const noexcept; - /** For column hire_date */ - ///Get the value of the column hire_date, returns the default value if the column is null - const ::trantor::Date &getValueOfHireDate() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr<::trantor::Date> &getHireDate() const noexcept; + Json::Value toJson() const; - Json::Value toJson() const; - private: - friend drogon::orm::Mapper; - std::shared_ptr id_; - std::shared_ptr jobId_; - std::shared_ptr jobTitle_; - std::shared_ptr departmentId_; - std::shared_ptr departmentName_; - std::shared_ptr managerId_; - std::shared_ptr managerFullName_; - std::shared_ptr firstName_; - std::shared_ptr lastName_; - std::shared_ptr<::trantor::Date> hireDate_; -}; -} // namespace org_chart + private: + friend drogon::orm::Mapper; + std::shared_ptr id_; + std::shared_ptr jobId_; + std::shared_ptr jobTitle_; + std::shared_ptr departmentId_; + std::shared_ptr departmentName_; + std::shared_ptr managerId_; + std::shared_ptr managerFullName_; + std::shared_ptr firstName_; + std::shared_ptr lastName_; + std::shared_ptr<::trantor::Date> hireDate_; + }; + } // namespace org_chart } // namespace drogon_model diff --git a/models/User.h b/models/User.h index 20e6dcc..a90ee62 100644 --- a/models/User.h +++ b/models/User.h @@ -26,202 +26,191 @@ namespace drogon { -namespace orm -{ -class DbClient; -using DbClientPtr = std::shared_ptr; -} + namespace orm + { + class DbClient; + using DbClientPtr = std::shared_ptr; + } } namespace drogon_model { -namespace org_chart -{ - -class User -{ - public: - struct Cols + namespace org_chart { - static const std::string _id; - static const std::string _username; - static const std::string _password; - }; - - const static int primaryKeyNumber; - const static std::string tableName; - const static bool hasPrimaryKey; - const static std::string primaryKeyName; - using PrimaryKeyType = int32_t; - const PrimaryKeyType &getPrimaryKey() const; - - /** - * @brief constructor - * @param r One row of records in the SQL query result. - * @param indexOffset Set the offset to -1 to access all columns by column names, - * otherwise access all columns by offsets. - * @note If the SQL is not a style of 'select * from table_name ...' (select all - * columns by an asterisk), please set the offset to -1. - */ - explicit User(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept; - - /** - * @brief constructor - * @param pJson The json object to construct a new instance. - */ - explicit User(const Json::Value &pJson) noexcept(false); - - /** - * @brief constructor - * @param pJson The json object to construct a new instance. - * @param pMasqueradingVector The aliases of table columns. - */ - User(const Json::Value &pJson, const std::vector &pMasqueradingVector) noexcept(false); - - User() = default; - - void updateByJson(const Json::Value &pJson) noexcept(false); - void updateByMasqueradedJson(const Json::Value &pJson, - const std::vector &pMasqueradingVector) noexcept(false); - static bool validateJsonForCreation(const Json::Value &pJson, std::string &err); - static bool validateMasqueradedJsonForCreation(const Json::Value &, - const std::vector &pMasqueradingVector, - std::string &err); - static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err); - static bool validateMasqueradedJsonForUpdate(const Json::Value &, - const std::vector &pMasqueradingVector, - std::string &err); - static bool validJsonOfField(size_t index, - const std::string &fieldName, - const Json::Value &pJson, - std::string &err, - bool isForCreation); - - /** For column id */ - ///Get the value of the column id, returns the default value if the column is null - const int32_t &getValueOfId() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getId() const noexcept; - ///Set the value of the column id - void setId(const int32_t &pId) noexcept; - - /** For column username */ - ///Get the value of the column username, returns the default value if the column is null - const std::string &getValueOfUsername() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getUsername() const noexcept; - ///Set the value of the column username - void setUsername(const std::string &pUsername) noexcept; - void setUsername(std::string &&pUsername) noexcept; - - /** For column password */ - ///Get the value of the column password, returns the default value if the column is null - const std::string &getValueOfPassword() const noexcept; - ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null - const std::shared_ptr &getPassword() const noexcept; - ///Set the value of the column password - void setPassword(const std::string &pPassword) noexcept; - void setPassword(std::string &&pPassword) noexcept; - - - static size_t getColumnNumber() noexcept { return 3; } - static const std::string &getColumnName(size_t index) noexcept(false); - - Json::Value toJson() const; - Json::Value toMasqueradedJson(const std::vector &pMasqueradingVector) const; - /// Relationship interfaces - private: - friend drogon::orm::Mapper; -#ifdef __cpp_impl_coroutine - friend drogon::orm::CoroMapper; -#endif - static const std::vector &insertColumns() noexcept; - void outputArgs(drogon::orm::internal::SqlBinder &binder) const; - const std::vector updateColumns() const; - void updateArgs(drogon::orm::internal::SqlBinder &binder) const; - ///For mysql or sqlite3 - void updateId(const uint64_t id); - std::shared_ptr id_; - std::shared_ptr username_; - std::shared_ptr password_; - struct MetaData - { - const std::string colName_; - const std::string colType_; - const std::string colDatabaseType_; - const ssize_t colLength_; - const bool isAutoVal_; - const bool isPrimaryKey_; - const bool notNull_; - }; - static const std::vector metaData_; - bool dirtyFlag_[3]={ false }; - public: - static const std::string &sqlForFindingByPrimaryKey() - { - static const std::string sql="select * from " + tableName + " where id = $1"; - return sql; - } - static const std::string &sqlForDeletingByPrimaryKey() - { - static const std::string sql="delete from " + tableName + " where id = $1"; - return sql; - } - std::string sqlForInserting(bool &needSelection) const - { - std::string sql="insert into " + tableName + " ("; - size_t parametersCount = 0; - needSelection = false; - sql += "id,"; - ++parametersCount; - if(dirtyFlag_[1]) - { - sql += "username,"; - ++parametersCount; - } - if(dirtyFlag_[2]) - { - sql += "password,"; - ++parametersCount; - } - needSelection=true; - if(parametersCount > 0) - { - sql[sql.length()-1]=')'; - sql += " values ("; - } - else - sql += ") values ("; - - int placeholder=1; - char placeholderStr[64]; - size_t n=0; - sql +="default,"; - if(dirtyFlag_[1]) - { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); - } - if(dirtyFlag_[2]) + class User { - n = sprintf(placeholderStr,"$%d,",placeholder++); - sql.append(placeholderStr, n); - } - if(parametersCount > 0) - { - sql.resize(sql.length() - 1); - } - if(needSelection) - { - sql.append(") returning *"); - } - else - { - sql.append(1, ')'); - } - LOG_TRACE << sql; - return sql; - } -}; -} // namespace org_chart + public: + struct Cols + { + static const std::string _id; + static const std::string _username; + static const std::string _password; + }; + + const static int primaryKeyNumber; + const static std::string tableName; + const static bool hasPrimaryKey; + const static std::string primaryKeyName; + using PrimaryKeyType = int32_t; + const PrimaryKeyType &getPrimaryKey() const; + + /** + * @brief constructor + * @param r One row of records in the SQL query result. + * @param indexOffset Set the offset to -1 to access all columns by column names, + * otherwise access all columns by offsets. + * @note If the SQL is not a style of 'select * from table_name ...' (select all + * columns by an asterisk), please set the offset to -1. + */ + explicit User(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept; + + /** + * @brief constructor + * @param pJson The json object to construct a new instance. + */ + explicit User(const Json::Value &pJson) noexcept(false); + + /** + * @brief constructor + * @param pJson The json object to construct a new instance. + * @param pMasqueradingVector The aliases of table columns. + */ + User(const Json::Value &pJson, const std::vector &pMasqueradingVector) noexcept(false); + + User() = default; + + void updateByJson(const Json::Value &pJson) noexcept(false); + void updateByMasqueradedJson(const Json::Value &pJson, + const std::vector &pMasqueradingVector) noexcept(false); + static bool validateJsonForCreation(const Json::Value &pJson, std::string &err); + static bool validateMasqueradedJsonForCreation(const Json::Value &, + const std::vector &pMasqueradingVector, + std::string &err); + static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err); + static bool validateMasqueradedJsonForUpdate(const Json::Value &, + const std::vector &pMasqueradingVector, + std::string &err); + static bool validJsonOfField(size_t index, + const std::string &fieldName, + const Json::Value &pJson, + std::string &err, + bool isForCreation); + + /** For column id */ + /// Get the value of the column id, returns the default value if the column is null + const int32_t &getValueOfId() const noexcept; + /// Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null + const std::shared_ptr &getId() const noexcept; + /// Set the value of the column id + void setId(const int32_t &pId) noexcept; + + /** For column username */ + /// Get the value of the column username, returns the default value if the column is null + const std::string &getValueOfUsername() const noexcept; + /// Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null + const std::shared_ptr &getUsername() const noexcept; + /// Set the value of the column username + void setUsername(const std::string &pUsername) noexcept; + void setUsername(std::string &&pUsername) noexcept; + + /** For column password */ + /// Get the value of the column password, returns the default value if the column is null + const std::string &getValueOfPassword() const noexcept; + /// Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null + const std::shared_ptr &getPassword() const noexcept; + /// Set the value of the column password + void setPassword(const std::string &pPassword) noexcept; + void setPassword(std::string &&pPassword) noexcept; + + static size_t getColumnNumber() noexcept { return 3; } + static const std::string &getColumnName(size_t index) noexcept(false); + + Json::Value toJson() const; + Json::Value toMasqueradedJson(const std::vector &pMasqueradingVector) const; + /// Relationship interfaces + private: + friend drogon::orm::Mapper; +#ifdef __cpp_impl_coroutine + friend drogon::orm::CoroMapper; +#endif + static const std::vector &insertColumns() noexcept; + void outputArgs(drogon::orm::internal::SqlBinder &binder) const; + const std::vector updateColumns() const; + void updateArgs(drogon::orm::internal::SqlBinder &binder) const; + /// For mysql or sqlite3 + void updateId(const uint64_t id); + std::shared_ptr id_; + std::shared_ptr username_; + std::shared_ptr password_; + struct MetaData + { + const std::string colName_; + const std::string colType_; + const std::string colDatabaseType_; + const ssize_t colLength_; + const bool isAutoVal_; + const bool isPrimaryKey_; + const bool notNull_; + }; + static const std::vector metaData_; + bool dirtyFlag_[3] = {false}; + + public: + static const std::string &sqlForFindingByPrimaryKey() + { + static const std::string sql = "select * from " + tableName + " where id = $1"; + return sql; + } + + static const std::string &sqlForDeletingByPrimaryKey() + { + static const std::string sql = "delete from " + tableName + " where id = $1"; + return sql; + } + std::string sqlForInserting(bool &needSelection) const + { + std::string sql = "INSERT INTO " + tableName + " ("; + size_t parametersCount = 0; + + // Build column list --------------------------------------------------- + if (dirtyFlag_[1]) // username + { + sql += "username,"; + ++parametersCount; + } + if (dirtyFlag_[2]) // password + { + sql += "`password`,"; + ++parametersCount; + } + + // Replace trailing comma with ')' and open VALUES --------------------- + if (parametersCount > 0) + { + sql.back() = ')'; + sql += " VALUES ("; + } + else + { + sql += ") VALUES ("; + } + + // Add ? placeholders in the same order -------------------------------- + if (dirtyFlag_[1]) + sql += "?,"; + if (dirtyFlag_[2]) + sql += "?,"; + + // Strip final comma and close VALUES ---------------------------------- + if (sql.back() == ',') + sql.back() = ')'; + else + sql += ')'; + + needSelection = false; // no RETURNING for MySQL + LOG_TRACE << sql; + return sql; + } + }; + } // namespace org_chart } // namespace drogon_model diff --git a/models/Users.cc b/models/Users.cc new file mode 100644 index 0000000..901c677 --- /dev/null +++ b/models/Users.cc @@ -0,0 +1,661 @@ +/** + * + * Users.cc + * DO NOT EDIT. This file is generated by drogon_ctl + * + */ + +#include "Users.h" +#include +#include + +using namespace drogon; +using namespace drogon::orm; +using namespace drogon_model::org_chart; + +const std::string Users::Cols::_id = "id"; +const std::string Users::Cols::_username = "username"; +const std::string Users::Cols::_password = "password"; +const std::string Users::primaryKeyName = "id"; +const bool Users::hasPrimaryKey = true; +const std::string Users::tableName = "users"; + +const std::vector Users::metaData_={ +{"id","uint64_t","bigint unsigned",8,1,1,1}, +{"username","std::string","varchar(50)",50,0,0,1}, +{"password","std::string","varchar(255)",255,0,0,1} +}; +const std::string &Users::getColumnName(size_t index) noexcept(false) +{ + assert(index < metaData_.size()); + return metaData_[index].colName_; +} +Users::Users(const Row &r, const ssize_t indexOffset) noexcept +{ + if(indexOffset < 0) + { + if(!r["id"].isNull()) + { + id_=std::make_shared(r["id"].as()); + } + if(!r["username"].isNull()) + { + username_=std::make_shared(r["username"].as()); + } + if(!r["password"].isNull()) + { + password_=std::make_shared(r["password"].as()); + } + } + else + { + size_t offset = (size_t)indexOffset; + if(offset + 3 > r.size()) + { + LOG_FATAL << "Invalid SQL result for this model"; + return; + } + size_t index; + index = offset + 0; + if(!r[index].isNull()) + { + id_=std::make_shared(r[index].as()); + } + index = offset + 1; + if(!r[index].isNull()) + { + username_=std::make_shared(r[index].as()); + } + index = offset + 2; + if(!r[index].isNull()) + { + password_=std::make_shared(r[index].as()); + } + } + +} + +Users::Users(const Json::Value &pJson, const std::vector &pMasqueradingVector) noexcept(false) +{ + if(pMasqueradingVector.size() != 3) + { + LOG_ERROR << "Bad masquerading vector"; + return; + } + if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0])) + { + dirtyFlag_[0] = true; + if(!pJson[pMasqueradingVector[0]].isNull()) + { + id_=std::make_shared((uint64_t)pJson[pMasqueradingVector[0]].asUInt64()); + } + } + if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) + { + dirtyFlag_[1] = true; + if(!pJson[pMasqueradingVector[1]].isNull()) + { + username_=std::make_shared(pJson[pMasqueradingVector[1]].asString()); + } + } + if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2])) + { + dirtyFlag_[2] = true; + if(!pJson[pMasqueradingVector[2]].isNull()) + { + password_=std::make_shared(pJson[pMasqueradingVector[2]].asString()); + } + } +} + +Users::Users(const Json::Value &pJson) noexcept(false) +{ + if(pJson.isMember("id")) + { + dirtyFlag_[0]=true; + if(!pJson["id"].isNull()) + { + id_=std::make_shared((uint64_t)pJson["id"].asUInt64()); + } + } + if(pJson.isMember("username")) + { + dirtyFlag_[1]=true; + if(!pJson["username"].isNull()) + { + username_=std::make_shared(pJson["username"].asString()); + } + } + if(pJson.isMember("password")) + { + dirtyFlag_[2]=true; + if(!pJson["password"].isNull()) + { + password_=std::make_shared(pJson["password"].asString()); + } + } +} + +void Users::updateByMasqueradedJson(const Json::Value &pJson, + const std::vector &pMasqueradingVector) noexcept(false) +{ + if(pMasqueradingVector.size() != 3) + { + LOG_ERROR << "Bad masquerading vector"; + return; + } + if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0])) + { + if(!pJson[pMasqueradingVector[0]].isNull()) + { + id_=std::make_shared((uint64_t)pJson[pMasqueradingVector[0]].asUInt64()); + } + } + if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) + { + dirtyFlag_[1] = true; + if(!pJson[pMasqueradingVector[1]].isNull()) + { + username_=std::make_shared(pJson[pMasqueradingVector[1]].asString()); + } + } + if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2])) + { + dirtyFlag_[2] = true; + if(!pJson[pMasqueradingVector[2]].isNull()) + { + password_=std::make_shared(pJson[pMasqueradingVector[2]].asString()); + } + } +} + +void Users::updateByJson(const Json::Value &pJson) noexcept(false) +{ + if(pJson.isMember("id")) + { + if(!pJson["id"].isNull()) + { + id_=std::make_shared((uint64_t)pJson["id"].asUInt64()); + } + } + if(pJson.isMember("username")) + { + dirtyFlag_[1] = true; + if(!pJson["username"].isNull()) + { + username_=std::make_shared(pJson["username"].asString()); + } + } + if(pJson.isMember("password")) + { + dirtyFlag_[2] = true; + if(!pJson["password"].isNull()) + { + password_=std::make_shared(pJson["password"].asString()); + } + } +} + +const uint64_t &Users::getValueOfId() const noexcept +{ + static const uint64_t defaultValue = uint64_t(); + if(id_) + return *id_; + return defaultValue; +} +const std::shared_ptr &Users::getId() const noexcept +{ + return id_; +} +void Users::setId(const uint64_t &pId) noexcept +{ + id_ = std::make_shared(pId); + dirtyFlag_[0] = true; +} +const typename Users::PrimaryKeyType & Users::getPrimaryKey() const +{ + assert(id_); + return *id_; +} + +const std::string &Users::getValueOfUsername() const noexcept +{ + static const std::string defaultValue = std::string(); + if(username_) + return *username_; + return defaultValue; +} +const std::shared_ptr &Users::getUsername() const noexcept +{ + return username_; +} +void Users::setUsername(const std::string &pUsername) noexcept +{ + username_ = std::make_shared(pUsername); + dirtyFlag_[1] = true; +} +void Users::setUsername(std::string &&pUsername) noexcept +{ + username_ = std::make_shared(std::move(pUsername)); + dirtyFlag_[1] = true; +} + +const std::string &Users::getValueOfPassword() const noexcept +{ + static const std::string defaultValue = std::string(); + if(password_) + return *password_; + return defaultValue; +} +const std::shared_ptr &Users::getPassword() const noexcept +{ + return password_; +} +void Users::setPassword(const std::string &pPassword) noexcept +{ + password_ = std::make_shared(pPassword); + dirtyFlag_[2] = true; +} +void Users::setPassword(std::string &&pPassword) noexcept +{ + password_ = std::make_shared(std::move(pPassword)); + dirtyFlag_[2] = true; +} + +void Users::updateId(const uint64_t id) +{ + id_ = std::make_shared(id); +} + +const std::vector &Users::insertColumns() noexcept +{ + static const std::vector inCols={ + "username", + "password" + }; + return inCols; +} + +void Users::outputArgs(drogon::orm::internal::SqlBinder &binder) const +{ + if(dirtyFlag_[1]) + { + if(getUsername()) + { + binder << getValueOfUsername(); + } + else + { + binder << nullptr; + } + } + if(dirtyFlag_[2]) + { + if(getPassword()) + { + binder << getValueOfPassword(); + } + else + { + binder << nullptr; + } + } +} + +const std::vector Users::updateColumns() const +{ + std::vector ret; + if(dirtyFlag_[1]) + { + ret.push_back(getColumnName(1)); + } + if(dirtyFlag_[2]) + { + ret.push_back(getColumnName(2)); + } + return ret; +} + +void Users::updateArgs(drogon::orm::internal::SqlBinder &binder) const +{ + if(dirtyFlag_[1]) + { + if(getUsername()) + { + binder << getValueOfUsername(); + } + else + { + binder << nullptr; + } + } + if(dirtyFlag_[2]) + { + if(getPassword()) + { + binder << getValueOfPassword(); + } + else + { + binder << nullptr; + } + } +} +Json::Value Users::toJson() const +{ + Json::Value ret; + if(getId()) + { + ret["id"]=(Json::UInt64)getValueOfId(); + } + else + { + ret["id"]=Json::Value(); + } + if(getUsername()) + { + ret["username"]=getValueOfUsername(); + } + else + { + ret["username"]=Json::Value(); + } + if(getPassword()) + { + ret["password"]=getValueOfPassword(); + } + else + { + ret["password"]=Json::Value(); + } + return ret; +} + +std::string Users::toString() const +{ + return toJson().toStyledString(); +} + +Json::Value Users::toMasqueradedJson( + const std::vector &pMasqueradingVector) const +{ + Json::Value ret; + if(pMasqueradingVector.size() == 3) + { + if(!pMasqueradingVector[0].empty()) + { + if(getId()) + { + ret[pMasqueradingVector[0]]=(Json::UInt64)getValueOfId(); + } + else + { + ret[pMasqueradingVector[0]]=Json::Value(); + } + } + if(!pMasqueradingVector[1].empty()) + { + if(getUsername()) + { + ret[pMasqueradingVector[1]]=getValueOfUsername(); + } + else + { + ret[pMasqueradingVector[1]]=Json::Value(); + } + } + if(!pMasqueradingVector[2].empty()) + { + if(getPassword()) + { + ret[pMasqueradingVector[2]]=getValueOfPassword(); + } + else + { + ret[pMasqueradingVector[2]]=Json::Value(); + } + } + return ret; + } + LOG_ERROR << "Masquerade failed"; + if(getId()) + { + ret["id"]=(Json::UInt64)getValueOfId(); + } + else + { + ret["id"]=Json::Value(); + } + if(getUsername()) + { + ret["username"]=getValueOfUsername(); + } + else + { + ret["username"]=Json::Value(); + } + if(getPassword()) + { + ret["password"]=getValueOfPassword(); + } + else + { + ret["password"]=Json::Value(); + } + return ret; +} + +bool Users::validateJsonForCreation(const Json::Value &pJson, std::string &err) +{ + if(pJson.isMember("id")) + { + if(!validJsonOfField(0, "id", pJson["id"], err, true)) + return false; + } + if(pJson.isMember("username")) + { + if(!validJsonOfField(1, "username", pJson["username"], err, true)) + return false; + } + else + { + err="The username column cannot be null"; + return false; + } + if(pJson.isMember("password")) + { + if(!validJsonOfField(2, "password", pJson["password"], err, true)) + return false; + } + else + { + err="The password column cannot be null"; + return false; + } + return true; +} +bool Users::validateMasqueradedJsonForCreation(const Json::Value &pJson, + const std::vector &pMasqueradingVector, + std::string &err) +{ + if(pMasqueradingVector.size() != 3) + { + err = "Bad masquerading vector"; + return false; + } + try { + if(!pMasqueradingVector[0].empty()) + { + if(pJson.isMember(pMasqueradingVector[0])) + { + if(!validJsonOfField(0, pMasqueradingVector[0], pJson[pMasqueradingVector[0]], err, true)) + return false; + } + } + if(!pMasqueradingVector[1].empty()) + { + if(pJson.isMember(pMasqueradingVector[1])) + { + if(!validJsonOfField(1, pMasqueradingVector[1], pJson[pMasqueradingVector[1]], err, true)) + return false; + } + else + { + err="The " + pMasqueradingVector[1] + " column cannot be null"; + return false; + } + } + if(!pMasqueradingVector[2].empty()) + { + if(pJson.isMember(pMasqueradingVector[2])) + { + if(!validJsonOfField(2, pMasqueradingVector[2], pJson[pMasqueradingVector[2]], err, true)) + return false; + } + else + { + err="The " + pMasqueradingVector[2] + " column cannot be null"; + return false; + } + } + } + catch(const Json::LogicError &e) + { + err = e.what(); + return false; + } + return true; +} +bool Users::validateJsonForUpdate(const Json::Value &pJson, std::string &err) +{ + if(pJson.isMember("id")) + { + if(!validJsonOfField(0, "id", pJson["id"], err, false)) + return false; + } + else + { + err = "The value of primary key must be set in the json object for update"; + return false; + } + if(pJson.isMember("username")) + { + if(!validJsonOfField(1, "username", pJson["username"], err, false)) + return false; + } + if(pJson.isMember("password")) + { + if(!validJsonOfField(2, "password", pJson["password"], err, false)) + return false; + } + return true; +} +bool Users::validateMasqueradedJsonForUpdate(const Json::Value &pJson, + const std::vector &pMasqueradingVector, + std::string &err) +{ + if(pMasqueradingVector.size() != 3) + { + err = "Bad masquerading vector"; + return false; + } + try { + if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0])) + { + if(!validJsonOfField(0, pMasqueradingVector[0], pJson[pMasqueradingVector[0]], err, false)) + return false; + } + else + { + err = "The value of primary key must be set in the json object for update"; + return false; + } + if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1])) + { + if(!validJsonOfField(1, pMasqueradingVector[1], pJson[pMasqueradingVector[1]], err, false)) + return false; + } + if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2])) + { + if(!validJsonOfField(2, pMasqueradingVector[2], pJson[pMasqueradingVector[2]], err, false)) + return false; + } + } + catch(const Json::LogicError &e) + { + err = e.what(); + return false; + } + return true; +} +bool Users::validJsonOfField(size_t index, + const std::string &fieldName, + const Json::Value &pJson, + std::string &err, + bool isForCreation) +{ + switch(index) + { + case 0: + if(pJson.isNull()) + { + err="The " + fieldName + " column cannot be null"; + return false; + } + if(isForCreation) + { + err="The automatic primary key cannot be set"; + return false; + } + if(!pJson.isUInt64()) + { + err="Type error in the "+fieldName+" field"; + return false; + } + break; + case 1: + if(pJson.isNull()) + { + err="The " + fieldName + " column cannot be null"; + return false; + } + if(!pJson.isString()) + { + err="Type error in the "+fieldName+" field"; + return false; + } + if(pJson.isString() && std::strlen(pJson.asCString()) > 50) + { + err="String length exceeds limit for the " + + fieldName + + " field (the maximum value is 50)"; + return false; + } + + break; + case 2: + if(pJson.isNull()) + { + err="The " + fieldName + " column cannot be null"; + return false; + } + if(!pJson.isString()) + { + err="Type error in the "+fieldName+" field"; + return false; + } + if(pJson.isString() && std::strlen(pJson.asCString()) > 255) + { + err="String length exceeds limit for the " + + fieldName + + " field (the maximum value is 255)"; + return false; + } + + break; + default: + err="Internal error in the server"; + return false; + } + return true; +} diff --git a/models/Users.h b/models/Users.h new file mode 100644 index 0000000..86a7815 --- /dev/null +++ b/models/Users.h @@ -0,0 +1,224 @@ +/** + * + * Users.h + * DO NOT EDIT. This file is generated by drogon_ctl + * + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#ifdef __cpp_impl_coroutine +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace drogon +{ +namespace orm +{ +class DbClient; +using DbClientPtr = std::shared_ptr; +} +} +namespace drogon_model +{ +namespace org_chart +{ + +class Users +{ + public: + struct Cols + { + static const std::string _id; + static const std::string _username; + static const std::string _password; + }; + + static const int primaryKeyNumber; + static const std::string tableName; + static const bool hasPrimaryKey; + static const std::string primaryKeyName; + using PrimaryKeyType = uint64_t; + const PrimaryKeyType &getPrimaryKey() const; + + /** + * @brief constructor + * @param r One row of records in the SQL query result. + * @param indexOffset Set the offset to -1 to access all columns by column names, + * otherwise access all columns by offsets. + * @note If the SQL is not a style of 'select * from table_name ...' (select all + * columns by an asterisk), please set the offset to -1. + */ + explicit Users(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept; + + /** + * @brief constructor + * @param pJson The json object to construct a new instance. + */ + explicit Users(const Json::Value &pJson) noexcept(false); + + /** + * @brief constructor + * @param pJson The json object to construct a new instance. + * @param pMasqueradingVector The aliases of table columns. + */ + Users(const Json::Value &pJson, const std::vector &pMasqueradingVector) noexcept(false); + + Users() = default; + + void updateByJson(const Json::Value &pJson) noexcept(false); + void updateByMasqueradedJson(const Json::Value &pJson, + const std::vector &pMasqueradingVector) noexcept(false); + static bool validateJsonForCreation(const Json::Value &pJson, std::string &err); + static bool validateMasqueradedJsonForCreation(const Json::Value &, + const std::vector &pMasqueradingVector, + std::string &err); + static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err); + static bool validateMasqueradedJsonForUpdate(const Json::Value &, + const std::vector &pMasqueradingVector, + std::string &err); + static bool validJsonOfField(size_t index, + const std::string &fieldName, + const Json::Value &pJson, + std::string &err, + bool isForCreation); + + /** For column id */ + ///Get the value of the column id, returns the default value if the column is null + const uint64_t &getValueOfId() const noexcept; + ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null + const std::shared_ptr &getId() const noexcept; + ///Set the value of the column id + void setId(const uint64_t &pId) noexcept; + + /** For column username */ + ///Get the value of the column username, returns the default value if the column is null + const std::string &getValueOfUsername() const noexcept; + ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null + const std::shared_ptr &getUsername() const noexcept; + ///Set the value of the column username + void setUsername(const std::string &pUsername) noexcept; + void setUsername(std::string &&pUsername) noexcept; + + /** For column password */ + ///Get the value of the column password, returns the default value if the column is null + const std::string &getValueOfPassword() const noexcept; + ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null + const std::shared_ptr &getPassword() const noexcept; + ///Set the value of the column password + void setPassword(const std::string &pPassword) noexcept; + void setPassword(std::string &&pPassword) noexcept; + + + static size_t getColumnNumber() noexcept { return 3; } + static const std::string &getColumnName(size_t index) noexcept(false); + + Json::Value toJson() const; + std::string toString() const; + Json::Value toMasqueradedJson(const std::vector &pMasqueradingVector) const; + /// Relationship interfaces + private: + friend drogon::orm::Mapper; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; + friend drogon::orm::BaseBuilder; +#ifdef __cpp_impl_coroutine + friend drogon::orm::CoroMapper; +#endif + static const std::vector &insertColumns() noexcept; + void outputArgs(drogon::orm::internal::SqlBinder &binder) const; + const std::vector updateColumns() const; + void updateArgs(drogon::orm::internal::SqlBinder &binder) const; + ///For mysql or sqlite3 + void updateId(const uint64_t id); + std::shared_ptr id_; + std::shared_ptr username_; + std::shared_ptr password_; + struct MetaData + { + const std::string colName_; + const std::string colType_; + const std::string colDatabaseType_; + const ssize_t colLength_; + const bool isAutoVal_; + const bool isPrimaryKey_; + const bool notNull_; + }; + static const std::vector metaData_; + bool dirtyFlag_[3]={ false }; + public: + static const std::string &sqlForFindingByPrimaryKey() + { + static const std::string sql="select * from " + tableName + " where id = ?"; + return sql; + } + + static const std::string &sqlForDeletingByPrimaryKey() + { + static const std::string sql="delete from " + tableName + " where id = ?"; + return sql; + } + std::string sqlForInserting(bool &needSelection) const + { + std::string sql="insert into " + tableName + " ("; + size_t parametersCount = 0; + needSelection = false; + sql += "id,"; + ++parametersCount; + if(dirtyFlag_[1]) + { + sql += "username,"; + ++parametersCount; + } + if(dirtyFlag_[2]) + { + sql += "password,"; + ++parametersCount; + } + needSelection=true; + if(parametersCount > 0) + { + sql[sql.length()-1]=')'; + sql += " values ("; + } + else + sql += ") values ("; + + sql +="default,"; + if(dirtyFlag_[1]) + { + sql.append("?,"); + + } + if(dirtyFlag_[2]) + { + sql.append("?,"); + + } + if(parametersCount > 0) + { + sql.resize(sql.length() - 1); + } + sql.append(1, ')'); + LOG_TRACE << sql; + return sql; + } +}; +} // namespace org_chart +} // namespace drogon_model diff --git a/models/model.json b/models/model.json index 8075c94..12a5637 100644 --- a/models/model.json +++ b/models/model.json @@ -1,24 +1,10 @@ { - //rdbms: server type, postgresql,mysql or sqlite3 - "rdbms": "postgresql", - //filename: sqlite3 db file name - //"filename":"", - //host: server address,localhost by default; - "host": "127.0.0.1", - //port: server port, 5432 by default; - "port": 5433, - //dbname: Database name; + "rdbms": "mysql", + "host": "127.0.0.1", + "port": 3306, "dbname": "org_chart", - //schema: valid for postgreSQL, "public" by default; - "schema": "public", - //user: User name - "user": "postgres", - //password or passwd: Password + "user": "org", "password": "password", - //client_encoding: The character set used by drogon_ctl. it is empty string by default which - //means use the default character set. - //"client_encoding": "", - //table: An array of tables to be modelized. if the array is empty, all revealed tables are modelized. "tables": [], "relationships": { "enabled": true, @@ -53,7 +39,6 @@ "target_key": "manager_id", "enable_reverse": false } - ] } } diff --git a/postman.json b/postman.json new file mode 100644 index 0000000..b4fb927 --- /dev/null +++ b/postman.json @@ -0,0 +1,2329 @@ +{ + "info": { + "_postman_id": "b1aa86f1-6962-4ebc-bf4c-1eef0eed99e1", + "name": "orgchatapi", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "25619313", + "_collection_link": "https://red-crater-507561.postman.co/workspace/Team-Workspace~6a72fc01-4ebc-42b7-82f8-18542c577ceb/collection/25619313-b1aa86f1-6962-4ebc-bf4c-1eef0eed99e1?action=share&source=collection_link&creator=25619313" + }, + "item": [ + { + "name": "auth", + "item": [ + { + "name": "Login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"admin1\",\n \"password\": \"password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/auth/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "auth", + "login" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"admin1\",\n \"password\": \"password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/auth/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "auth", + "login" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "201" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 09:21:52 GMT" + } + ], + "cookie": [], + "body": "{\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwNTY1MTIsImlhdCI6MTc1MjA1MjkxMiwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMyJ9.-K3o-RBkiQEvfXFw6eePWFej08AMPm7lo-O8z65VSFM\",\n \"username\": \"admin3ads2\"\n}" + }, + { + "name": "400 - User not found", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"admin2\",\n \"password\": \"password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/auth/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "auth", + "login" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "26" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 10:28:15 GMT" + } + ], + "cookie": [], + "body": "{\n \"error\": \"user not found\"\n}" + }, + { + "name": "401 - wrong password", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"admin1\",\n \"password\": \"passwdqsord\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/auth/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "auth", + "login" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "46" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 10:28:57 GMT" + } + ], + "cookie": [], + "body": "{\n \"error\": \"username and password do not match\"\n}" + } + ] + }, + { + "name": "resgister user", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/auth/deregister", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "auth", + "deregister" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/auth/register", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "auth", + "register" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "203" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 10:27:40 GMT" + } + ], + "cookie": [], + "body": "{\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwNjA0NjAsImlhdCI6MTc1MjA1Njg2MCwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMCJ9.N7JlIN3qyYi3ku4cLTR8qhTST0a37W3L2Uv50mOIC0s\",\n \"username\": \"admin3adwes2\"\n}" + }, + { + "name": "400 - username taken", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"admin1\",\n \"password\": \"passwordss\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/auth/register", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "auth", + "register" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "29" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 09:21:35 GMT" + } + ], + "cookie": [], + "body": "{\n \"error\": \"username is taken\"\n}" + }, + { + "name": "200 deregister", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/auth/deregister", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "auth", + "deregister" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "44" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Thu, 17 Jul 2025 07:04:40 GMT" + } + ], + "cookie": [], + "body": "{\n \"message\": \"user deregistered successfully\"\n}" + } + ] + } + ] + }, + { + "name": "person", + "item": [ + { + "name": "Get All persons", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwNjA0NjAsImlhdCI6MTc1MjA1Njg2MCwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMCJ9.N7JlIN3qyYi3ku4cLTR8qhTST0a37W3L2Uv50mOIC0s", + "disabled": true + } + ], + "url": { + "raw": "http://localhost:3000/persons?offset=1&limit=25&sort_field=id&sort_order=asc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons" + ], + "query": [ + { + "key": "offset", + "value": "1" + }, + { + "key": "limit", + "value": "25" + }, + { + "key": "sort_field", + "value": "id" + }, + { + "key": "sort_order", + "value": "asc" + } + ] + } + }, + "response": [] + }, + { + "name": "Get A Person", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/13", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "13" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/12", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "12" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "200" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 10:25:19 GMT" + } + ], + "cookie": [], + "body": "{\n \"department\": {\n \"id\": 2,\n \"name\": \"Infrastructure\"\n },\n \"first_name\": \"Yancey\",\n \"hire_date\": \"2022-03-02\",\n \"id\": 12,\n \"job\": {\n \"id\": 4,\n \"title\": \"E5\"\n },\n \"last_name\": \"Trenton\",\n \"manager\": {\n \"full_name\": \"Sterling Haley\",\n \"id\": 8\n }\n}" + }, + { + "name": "404", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/56", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "56" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "30" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 10:26:45 GMT" + } + ], + "cookie": [], + "body": "{\n \"error\": \"resource not found\"\n}" + }, + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/12", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "12" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "200" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Tue, 15 Jul 2025 08:47:25 GMT" + } + ], + "cookie": [], + "body": "{\n \"department\": {\n \"id\": 2,\n \"name\": \"Infrastructure\"\n },\n \"first_name\": \"Yancey\",\n \"hire_date\": \"2022-03-02\",\n \"id\": 12,\n \"job\": {\n \"id\": 4,\n \"title\": \"E5\"\n },\n \"last_name\": \"Trenton\",\n \"manager\": {\n \"full_name\": \"Sterling Haley\",\n \"id\": 8\n }\n}" + } + ] + }, + { + "name": "Get report of person", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/25/reports", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "25", + "reports" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/1/reports", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "1", + "reports" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "485" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 10:43:10 GMT" + } + ], + "cookie": [], + "body": "[\n {\n \"department_id\": 1,\n \"first_name\": \"Sabryna\",\n \"hire_date\": \"2014-02-01\",\n \"id\": 1,\n \"job_id\": 1,\n \"last_name\": \"Peers\",\n \"manager_id\": 1\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Tayler\",\n \"hire_date\": \"2018-04-07\",\n \"id\": 2,\n \"job_id\": 2,\n \"last_name\": \"Shantee\",\n \"manager_id\": 1\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Madonna\",\n \"hire_date\": \"2018-03-08\",\n \"id\": 3,\n \"job_id\": 2,\n \"last_name\": \"Axl\",\n \"manager_id\": 1\n },\n {\n \"department_id\": 2,\n \"first_name\": \"Sterling\",\n \"hire_date\": \"2019-11-02\",\n \"id\": 8,\n \"job_id\": 2,\n \"last_name\": \"Haley\",\n \"manager_id\": 1\n }\n]" + }, + { + "name": "500 - personId not present", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/25/reports", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "25", + "reports" + ] + } + }, + "status": "Internal Server Error", + "code": 500, + "_postman_previewlanguage": "html", + "header": [ + { + "key": "content-length", + "value": "0" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 10:50:04 GMT" + } + ], + "cookie": [], + "body": null + } + ] + }, + { + "name": "Create Person", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"job_id\": 4,\n \"department_id\": 1,\n \"first_name\": \"cap\",\n \"last_name\": \"Kamaaa\",\n \"hire_date\": \"2029-03-02\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"job_id\": 4,\n \"department_id\": 1,\n \"first_name\": \"cap\",\n \"last_name\": \"Kam\",\n \"manager_id\": 2,\n \"hire_date\": \"2029-03-02\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "115" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Thu, 10 Jul 2025 10:22:55 GMT" + } + ], + "cookie": [], + "body": "{\n \"department_id\": 1,\n \"first_name\": \"cap\",\n \"hire_date\": \"2029-03-02\",\n \"id\": 16,\n \"job_id\": 4,\n \"last_name\": \"Kam\",\n \"manager_id\": 2\n}" + }, + { + "name": "400 hire date", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"job_id\": 4,\n \"department_id\": 1,\n \"first_name\": \"cap\",\n \"last_name\": \"Kam\",\n \"manager_id\": 2,\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "35" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Thu, 10 Jul 2025 10:28:11 GMT" + } + ], + "cookie": [], + "body": "{\n \"error\": \"hire_date is compulsory\"\n}" + }, + { + "name": "400 depart id", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"job_id\": 4,\n \"first_name\": \"cap\",\n \"last_name\": \"Kam\",\n \"manager_id\": 2,\n \"hire_date\": \"2029-03-02\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "39" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Thu, 10 Jul 2025 10:28:41 GMT" + } + ], + "cookie": [], + "body": "{\n \"error\": \"department_id is compulsory\"\n}" + }, + { + "name": "400 job id", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"department_id\": 1,\n \"first_name\": \"cap\",\n \"last_name\": \"Kam\",\n \"manager_id\": 2,\n \"hire_date\": \"2029-03-02\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "32" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Thu, 10 Jul 2025 10:29:23 GMT" + } + ], + "cookie": [], + "body": "{\n \"error\": \"job_id is compulsory\"\n}" + } + ] + }, + { + "name": "delete person", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "1" + ] + } + }, + "response": [ + { + "name": "204", + "originalRequest": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:3000/persons/13", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "13" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "html", + "header": [ + { + "key": "content-length", + "value": "0" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 10:53:22 GMT" + } + ], + "cookie": [], + "body": null + } + ] + }, + { + "name": "Update Person", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manager_id\": \"1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/persons/14", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "14" + ] + } + }, + "response": [ + { + "name": "422", + "originalRequest": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manager_id\": \"50\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/persons/12", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "12" + ] + } + }, + "status": "Unprocessable Entity", + "code": 422, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "33" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Thu, 10 Jul 2025 10:45:25 GMT" + } + ], + "cookie": [], + "body": "{\n \"error\": \"manager_id is invalid\"\n}" + }, + { + "name": "204", + "originalRequest": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manager_id\": \"2\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/persons/12", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "persons", + "12" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "html", + "header": [ + { + "key": "content-length", + "value": "0" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Thu, 10 Jul 2025 10:45:37 GMT" + } + ], + "cookie": [], + "body": null + } + ] + } + ] + }, + { + "name": "department", + "item": [ + { + "name": "get all", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/departments?offset=0&limit=25&sort_field=id&sort_order=asc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments" + ], + "query": [ + { + "key": "offset", + "value": "0" + }, + { + "key": "limit", + "value": "25" + }, + { + "key": "sort_field", + "value": "id" + }, + { + "key": "sort_order", + "value": "asc" + } + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/departments?offset=0&limit=25&sort_field=id&sort_order=asc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments" + ], + "query": [ + { + "key": "offset", + "value": "0" + }, + { + "key": "limit", + "value": "25" + }, + { + "key": "sort_field", + "value": "id" + }, + { + "key": "sort_order", + "value": "asc" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "60" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:07:13 GMT" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 1,\n \"name\": \"Product\"\n },\n {\n \"id\": 2,\n \"name\": \"Infrastructure\"\n }\n]" + } + ] + }, + { + "name": "get", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/departments/3", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments", + "3" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/departments/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments", + "1" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "25" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:07:51 GMT" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"name\": \"Product\"\n}" + }, + { + "name": "404", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/departments/3", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments", + "3" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "html", + "header": [ + { + "key": "content-length", + "value": "0" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:08:18 GMT" + } + ], + "cookie": [], + "body": null + } + ] + }, + { + "name": "get persons", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/departments/1/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments", + "1", + "persons" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/departments/1/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments", + "1", + "persons" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "966" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:10:13 GMT" + } + ], + "cookie": [], + "body": "[\n {\n \"department_id\": 1,\n \"first_name\": \"Sabryna\",\n \"hire_date\": \"2014-02-01\",\n \"id\": 1,\n \"job_id\": 1,\n \"last_name\": \"Peers\",\n \"manager_id\": 1\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Tayler\",\n \"hire_date\": \"2018-04-07\",\n \"id\": 2,\n \"job_id\": 2,\n \"last_name\": \"Shantee\",\n \"manager_id\": 1\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Madonna\",\n \"hire_date\": \"2018-03-08\",\n \"id\": 3,\n \"job_id\": 2,\n \"last_name\": \"Axl\",\n \"manager_id\": 1\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Marcia\",\n \"hire_date\": \"2020-01-11\",\n \"id\": 4,\n \"job_id\": 4,\n \"last_name\": \"Stuart\",\n \"manager_id\": 2\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Cliff\",\n \"hire_date\": \"2021-02-15\",\n \"id\": 5,\n \"job_id\": 3,\n \"last_name\": \"Rosalind\",\n \"manager_id\": 2\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Lake\",\n \"hire_date\": \"2022-05-21\",\n \"id\": 6,\n \"job_id\": 3,\n \"last_name\": \"Philippa\",\n \"manager_id\": 3\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Wynne\",\n \"hire_date\": \"2021-12-31\",\n \"id\": 7,\n \"job_id\": 3,\n \"last_name\": \"Walker\",\n \"manager_id\": 3\n },\n {\n \"department_id\": 1,\n \"first_name\": \"Charan\",\n \"hire_date\": \"2022-03-02\",\n \"id\": 17,\n \"job_id\": 4,\n \"last_name\": \"Kam\",\n \"manager_id\": 2\n }\n]" + } + ] + }, + { + "name": "create", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"New Product\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/departments", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments" + ] + } + }, + "response": [ + { + "name": "201", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"New Product\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/departments", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "29" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:11:54 GMT" + } + ], + "cookie": [], + "body": "{\n \"id\": 3,\n \"name\": \"New Product\"\n}" + } + ] + }, + { + "name": "put", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\": \"new2\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/departments/3", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments", + "3" + ] + } + }, + "response": [] + }, + { + "name": "delete", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/departments/3", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments", + "3" + ] + } + }, + "response": [ + { + "name": "201", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/departments/3", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "departments", + "3" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "html", + "header": [ + { + "key": "content-length", + "value": "0" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:13:39 GMT" + } + ], + "cookie": [], + "body": null + } + ] + } + ] + }, + { + "name": "job", + "item": [ + { + "name": "getall", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI0OTQ2MDgsImlhdCI6MTc1MjQ5MTAwOCwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMTIifQ.Uq1-QBDckUqFs8mjsC0VQceT3BnsDcO3DHzk4kV24OE", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/jobs?offset=1&limit=25&sort_field=id&sort_order=asc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs" + ], + "query": [ + { + "key": "offset", + "value": "1" + }, + { + "key": "limit", + "value": "25" + }, + { + "key": "sort_field", + "value": "id" + }, + { + "key": "sort_order", + "value": "asc" + }, + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "disabled": true + } + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/jobs?offset=1&limit=25&sort_field=id&sort_order=asc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs" + ], + "query": [ + { + "key": "offset", + "value": "1" + }, + { + "key": "limit", + "value": "25" + }, + { + "key": "sort_field", + "value": "id" + }, + { + "key": "sort_order", + "value": "asc" + }, + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "disabled": true + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "67" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:20:06 GMT" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 2,\n \"title\": \"M1\"\n },\n {\n \"id\": 3,\n \"title\": \"E4\"\n },\n {\n \"id\": 4,\n \"title\": \"E5\"\n }\n]" + } + ] + }, + { + "name": "get", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/jobs/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs", + "1" + ] + } + }, + "response": [ + { + "name": "201", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/jobs/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs", + "1" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "22" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:20:54 GMT" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"title\": \"CEO\"\n}" + } + ] + }, + { + "name": "get persons", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/jobs/1/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs", + "1", + "persons" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/jobs/2/persons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs", + "2", + "persons" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "122" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:56:03 GMT" + } + ], + "cookie": [], + "body": "[\n {\n \"department_id\": 1,\n \"first_name\": \"Sabryna\",\n \"hire_date\": \"2014-02-01\",\n \"id\": 1,\n \"job_id\": 1,\n \"last_name\": \"Peers\",\n \"manager_id\": 1\n }\n]" + } + ] + }, + { + "name": "create job", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"poen\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/jobs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs" + ] + } + }, + "response": [ + { + "name": "201", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"poen\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/jobs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-length", + "value": "23" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:57:47 GMT" + } + ], + "cookie": [], + "body": "{\n \"id\": 5,\n \"title\": \"poen\"\n}" + } + ] + }, + { + "name": "update job", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"poen1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/jobs/5", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs", + "5" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"poen1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3000/jobs/5", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs", + "5" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "html", + "header": [ + { + "key": "content-length", + "value": "0" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:58:44 GMT" + } + ], + "cookie": [], + "body": null + } + ] + }, + { + "name": "delete", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTI1NzI3NDcsImlhdCI6MTc1MjU2OTE0NywiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9._pTqhslpZmWpvZwPS0e3E6hdRAdLGxowo_YR7BGt2MM", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/jobs/5", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs", + "5" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwOTUzOTUsImlhdCI6MTc1MjA5MTc5NSwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMiJ9.0MGnGsfZwpyaqTEKYeikMBf-8XTIsYbQvz-IHyF8Czk", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:3000/jobs/5", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "jobs", + "5" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "html", + "header": [ + { + "key": "content-length", + "value": "0" + }, + { + "key": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "key": "server", + "value": "drogon/1.7.5" + }, + { + "key": "date", + "value": "Wed, 09 Jul 2025 20:59:39 GMT" + } + ], + "cookie": [], + "body": null + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/schema.json b/schema.json new file mode 100644 index 0000000..9c0a6a7 --- /dev/null +++ b/schema.json @@ -0,0 +1,1304 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Org Chat API", + "description": "API for managing users and organizational chat data, including authentication, person directory, departments, and job roles. This API allows users to log in, register, and manage various organizational records.", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:3000", + "description": "Local Development Server" + } + ], + "tags": [ + { + "name": "Authentication", + "description": "User authentication and registration operations for accessing the API." + }, + { + "name": "Persons", + "description": "Operations related to managing person/employee records within the organization." + }, + { + "name": "Departments", + "description": "Operations related to managing organizational departments." + }, + { + "name": "Jobs", + "description": "Operations related to managing job titles and roles." + } + ], + "paths": { + "/auth/login": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Logs in a user", + "description": "Authenticates a user with a provided username and password. On successful authentication, a JWT access token and username are returned.", + "requestBody": { + "description": "User credentials for login.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + }, + "example": { + "username": "admin3adwes2", + "password": "passwdqsord" + } + } + } + }, + "responses": { + "200": { + "description": "Successful login, returns a JWT token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthSuccessResponse" + }, + "example": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwNTY1MTIsImlhdCI6MTc1MjA1MjkxMiwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMyJ9.-K3o-RBkiQEvfXFw6eePWFej08AMPm7lo-O8z65VSFM", + "username": "admin3ads2" + } + } + } + }, + "400": { + "description": "Bad Request, indicating the user was not found. (As per Postman example)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "user not found" + } + } + } + }, + "401": { + "description": "Unauthorized, indicating incorrect password.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "username and password do not match" + } + } + } + } + } + } + }, + "/auth/register": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Registers a new user", + "description": "Creates a new user account with a unique username and password. Upon successful registration, a JWT token is returned.", + "requestBody": { + "description": "User details for registration.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterRequest" + }, + "example": { + "username": "admin3adwes2", + "password": "password" + } + } + } + }, + "responses": { + "201": { + "description": "User successfully registered and logged in.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthSuccessResponse" + }, + "example": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwNjA0NjAsImlhdCI6MTc1MjA1Njg2MCwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMCJ9.N7JlIN3qyYi3ku4cLTR8qhTST0a37W3L2Uv50mOIC0s", + "username": "admin3adwes2" + } + } + } + }, + "422": { + "description": "Unprocessable Entity, typically if the username is already taken. (Semantic error)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "username is taken" + } + } + } + } + } + } + }, + "/auth/deregister": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Deregister (delete) a user", + "description": "Deletes a user account. Authentication requirements are implementation-defined; typically requires valid credentials in the body.", + "operationId": "deregisterUser", + "requestBody": { + "description": "User credentials to confirm deregistration.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeregisterRequest" + }, + "example": { + "username": "newuser", + "password": "password" + } + } + } + }, + "responses": { + "200": { + "description": "User deregistered successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + }, + "example": { + "message": "user deregistered successfully" + } + } + } + }, + "400": { + "description": "User not found or invalid credentials.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "notFound": { + "summary": "User not found", + "value": { + "error": "user not found" + } + }, + "badCreds": { + "summary": "Bad credentials", + "value": { + "error": "username and password do not match" + } + } + } + } + } + } + } + } + }, + "/persons": { + "get": { + "tags": [ + "Persons" + ], + "summary": "Get all persons", + "description": "Retrieves a paginated and sortable list of all persons (employees) in the system. Requires authentication.", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "offset", + "in": "query", + "description": "The number of items to skip before starting to collect the result set.", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 0, + "default": 0 + }, + "example": 1 + }, + { + "name": "limit", + "in": "query", + "description": "The maximum number of items to return.", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1, + "default": 25 + }, + "example": 25 + }, + { + "name": "sort_field", + "in": "query", + "description": "The field by which to sort the results.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "id", + "first_name", + "last_name", + "hire_date", + "job_id", + "department_id", + "manager_id" + ], + "default": "id" + }, + "example": "id" + }, + { + "name": "sort_order", + "in": "query", + "description": "The order in which to sort the results ('asc' for ascending, 'desc' for descending).", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + }, + "example": "asc" + } + ], + "responses": { + "200": { + "description": "A successful response with a list of persons.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDetail" + } + }, + "example": [ + { + "department": { + "id": 2, + "name": "Infrastructure" + }, + "first_name": "Yancey", + "hire_date": "2022-03-02", + "id": 12, + "job": { + "id": 4, + "title": "E5" + }, + "last_name": "Trenton", + "manager": { + "full_name": "Sterling Haley", + "id": 8 + } + } + ] + } + } + }, + "401": { + "description": "Unauthorized, typically due to a missing or invalid authentication token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + } + } + }, + "post": { + "tags": [ + "Persons" + ], + "summary": "Create a new person", + "description": "Adds a new person/employee record to the system. Requires authentication.", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "description": "Details of the person to be created.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePersonRequest" + }, + "example": { + "job_id": 4, + "department_id": 1, + "first_name": "cap", + "last_name": "Kamaaa", + "hire_date": "2029-03-02" + } + } + } + }, + "responses": { + "201": { + "description": "Person successfully created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + }, + "example": { + "id": 1, + "job_id": 4, + "department_id": 1, + "first_name": "cap", + "last_name": "Kam", + "manager_id": 2, + "hire_date": "2029-03-02" + } + } + } + }, + "422": { + "description": "Unprocessable Entity, due to invalid input data or missing required fields within a syntactically correct request. (Semantic error)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Invalid input data" + } + } + } + }, + "401": { + "description": "Unauthorized, due to a missing or invalid authentication token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + } + } + } + }, + "/persons/{personId}": { + "get": { + "tags": [ + "Persons" + ], + "summary": "Get a single person by ID", + "description": "Retrieves the detailed information of a specific person using their unique ID.", + "parameters": [ + { + "name": "personId", + "in": "path", + "description": "Unique identifier of the person to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 13 + } + ], + "responses": { + "200": { + "description": "Successful response with the details of the person.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonDetail" + }, + "example": { + "department": { + "id": 2, + "name": "Infrastructure" + }, + "first_name": "Yancey", + "hire_date": "2022-03-02", + "id": 12, + "job": { + "id": 4, + "title": "E5" + }, + "last_name": "Trenton", + "manager": { + "full_name": "Sterling Haley", + "id": 8 + } + } + } + } + }, + "404": { + "description": "Not Found, if a person with the specified ID does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "resource not found" + } + } + } + } + } + } + }, + "/persons/{personId}/reports": { + "get": { + "tags": [ + "Persons" + ], + "summary": "Get direct reports of a person", + "description": "Retrieves a list of all persons who directly report to the specified person (manager).", + "parameters": [ + { + "name": "personId", + "in": "path", + "description": "ID of the manager whose direct reports are to be retrieved.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 25 + } + ], + "responses": { + "200": { + "description": "A successful response with a list of direct reports.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonSummary" + } + }, + "example": [ + { + "department_id": 1, + "first_name": "Sabryna", + "hire_date": "2014-02-01", + "id": 1, + "job_id": 1, + "last_name": "Peers", + "manager_id": 1 + }, + { + "department_id": 1, + "first_name": "Tayler", + "hire_date": "2018-04-07", + "id": 2, + "job_id": 2, + "last_name": "Shantee", + "manager_id": 1 + }, + { + "department_id": 1, + "first_name": "Madonna", + "hire_date": "2018-03-08", + "id": 3, + "job_id": 2, + "last_name": "Axl", + "manager_id": 1 + }, + { + "department_id": 2, + "first_name": "Sterling", + "hire_date": "2019-11-02", + "id": 8, + "job_id": 2, + "last_name": "Haley", + "manager_id": 1 + } + ] + } + } + }, + "404": { + "description": "Not Found, if the specified person (manager) does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "resource not found" + } + } + } + }, + "500": { + "description": "Internal Server Error, potentially indicating the person ID is not present in the system or another server-side issue. (As per Postman example)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + } + } + }, + "/departments": { + "get": { + "tags": [ + "Departments" + ], + "summary": "Get all departments", + "description": "Retrieves a list of all organizational departments.", + "responses": { + "200": { + "description": "A successful response with a list of departments.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Department" + } + }, + "example": [ + { + "id": 1, + "name": "Engineering" + }, + { + "id": 2, + "name": "Infrastructure" + } + ] + } + } + } + } + }, + "post": { + "tags": [ + "Departments" + ], + "summary": "Create a new department", + "description": "Adds a new department to the system.", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "description": "Details of the department to be created.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDepartmentRequest" + }, + "example": { + "name": "New Department" + } + } + } + }, + "responses": { + "201": { + "description": "Department successfully created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Department" + }, + "example": { + "id": 3, + "name": "New Department" + } + } + } + }, + "422": { + "description": "Unprocessable Entity, due to invalid input data (e.g., department name already exists or is empty).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "duplicateName": { + "summary": "Duplicate Department Name", + "value": { + "error": "Department name already exists" + } + }, + "missingName": { + "summary": "Missing Department Name", + "value": { + "error": "Department name is required" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized, due to a missing or invalid authentication token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + } + } + } + }, + "/departments/{departmentId}": { + "get": { + "tags": [ + "Departments" + ], + "summary": "Get a single department by ID", + "description": "Retrieves the detailed information of a specific department using its unique ID.", + "parameters": [ + { + "name": "departmentId", + "in": "path", + "description": "Unique identifier of the department to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 1 + } + ], + "responses": { + "200": { + "description": "Successful response with the details of the department.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Department" + }, + "example": { + "id": 1, + "name": "Engineering" + } + } + } + }, + "404": { + "description": "Not Found, if a department with the specified ID does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "resource not found" + } + } + } + } + } + } + }, + "/jobs": { + "get": { + "tags": [ + "Jobs" + ], + "summary": "Get all jobs", + "description": "Retrieves a list of all available job titles.", + "responses": { + "200": { + "description": "A successful response with a list of jobs.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Job" + } + }, + "example": [ + { + "id": 1, + "title": "Staff Engineer" + }, + { + "id": 2, + "title": "Senior Engineer" + }, + { + "id": 3, + "title": "Junior Engineer" + } + ] + } + } + } + } + }, + "post": { + "tags": [ + "Jobs" + ], + "summary": "Create a new job title", + "description": "Adds a new job title to the system.", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "description": "Details of the job title to be created.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateJobRequest" + }, + "example": { + "title": "Lead Software Engineer" + } + } + } + }, + "responses": { + "201": { + "description": "Job title successfully created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Job" + }, + "example": { + "id": 5, + "title": "Lead Software Engineer" + } + } + } + }, + "422": { + "description": "Unprocessable Entity, due to invalid input data (e.g., job title already exists or is empty).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "duplicateTitle": { + "summary": "Duplicate Job Title", + "value": { + "error": "Job title already exists" + } + }, + "missingTitle": { + "summary": "Missing Job Title", + "value": { + "error": "Job title is required" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized, due to a missing or invalid authentication token.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + } + } + } + }, + "/jobs/{jobId}": { + "get": { + "tags": [ + "Jobs" + ], + "summary": "Get a single job by ID", + "description": "Retrieves the detailed information of a specific job title using its unique ID.", + "parameters": [ + { + "name": "jobId", + "in": "path", + "description": "Unique identifier of the job to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 2 + } + ], + "responses": { + "200": { + "description": "Successful response with the details of the job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Job" + }, + "example": { + "id": 2, + "title": "Senior Engineer" + } + } + } + }, + "404": { + "description": "Not Found, if a job with the specified ID does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "resource not found" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "LoginRequest": { + "type": "object", + "title": "Login Request", + "description": "Schema for user login request.", + "properties": { + "username": { + "type": "string", + "description": "The unique username of the user.", + "example": "admin3adwes2" + }, + "password": { + "type": "string", + "format": "password", + "description": "The password for the user account.", + "example": "passwdqsord" + } + }, + "required": [ + "username", + "password" + ] + }, + "RegisterRequest": { + "type": "object", + "title": "Register Request", + "description": "Schema for new user registration request.", + "properties": { + "username": { + "type": "string", + "description": "The desired unique username for the new account.", + "example": "admin3adwes2" + }, + "password": { + "type": "string", + "format": "password", + "description": "The password for the new user account.", + "example": "password" + } + }, + "required": [ + "username", + "password" + ] + }, + "AuthSuccessResponse": { + "type": "object", + "title": "Authentication Success Response", + "description": "Schema for a successful authentication or registration response.", + "properties": { + "token": { + "type": "string", + "description": "A JSON Web Token (JWT) used for subsequent API authentication.", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE3NTIwNTY1MTIsImlhdCI6MTc1MjA1MjkxMiwiaXNzIjoiYXV0aDAiLCJ1c2VyX2lkIjoiMyJ9.-K3o-RBkiQEvfXFw6eePWFej08AMPm7lo-O8z65VSFM" + }, + "username": { + "type": "string", + "description": "The username of the authenticated user.", + "example": "admin3ads2" + } + }, + "required": [ + "token", + "username" + ] + }, + "ErrorResponse": { + "type": "object", + "title": "Error Response", + "description": "Generic schema for API error responses.", + "properties": { + "error": { + "type": "string", + "description": "A descriptive error message.", + "example": "user not found" + } + }, + "required": [ + "error" + ] + }, + "Person": { + "type": "object", + "title": "Person", + "description": "Basic schema for a person/employee record.", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "The unique identifier for the person." + }, + "department_id": { + "type": "integer", + "format": "int64", + "description": "The ID of the department the person belongs to." + }, + "first_name": { + "type": "string", + "description": "The first name of the person." + }, + "hire_date": { + "type": "string", + "format": "date", + "description": "The date when the person was hired (YYYY-MM-DD)." + }, + "job_id": { + "type": "integer", + "format": "int64", + "description": "The ID of the job title held by the person." + }, + "last_name": { + "type": "string", + "description": "The last name of the person." + }, + "manager_id": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "The ID of the person's direct manager. Can be null if the person is a top-level manager." + } + }, + "required": [ + "department_id", + "first_name", + "hire_date", + "job_id", + "last_name" + ] + }, + "CreatePersonRequest": { + "type": "object", + "title": "Create Person Request", + "description": "Schema for creating a new person record.", + "properties": { + "job_id": { + "type": "integer", + "format": "int64", + "description": "The ID of the job title for the new person." + }, + "department_id": { + "type": "integer", + "format": "int64", + "description": "The ID of the department for the new person." + }, + "first_name": { + "type": "string", + "description": "The first name of the new person." + }, + "last_name": { + "type": "string", + "description": "The last name of the new person." + }, + "hire_date": { + "type": "string", + "format": "date", + "description": "The hire date for the new person (YYYY-MM-DD)." + }, + "manager_id": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "Optional: The ID of the manager for the new person." + } + }, + "required": [ + "job_id", + "department_id", + "first_name", + "last_name", + "hire_date" + ] + }, + "PersonDetail": { + "type": "object", + "title": "Person Detail", + "description": "Detailed schema for a single person, including nested department, job, and manager information.", + "properties": { + "department": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "The ID of the department." + }, + "name": { + "type": "string", + "description": "The name of the department." + } + }, + "required": [ + "id", + "name" + ] + }, + "first_name": { + "type": "string", + "description": "The first name of the person." + }, + "hire_date": { + "type": "string", + "format": "date", + "description": "The hire date of the person (YYYY-MM-DD)." + }, + "id": { + "type": "integer", + "format": "int64", + "description": "The unique identifier of the person." + }, + "job": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "The ID of the job title." + }, + "title": { + "type": "string", + "description": "The title of the job." + } + }, + "required": [ + "id", + "title" + ] + }, + "last_name": { + "type": "string", + "description": "The last name of the person." + }, + "manager": { + "type": "object", + "properties": { + "full_name": { + "type": "string", + "description": "The full name of the person's manager." + }, + "id": { + "type": "integer", + "format": "int64", + "description": "The ID of the person's manager." + } + }, + "nullable": true, + "description": "Information about the person's direct manager." + } + }, + "required": [ + "department", + "first_name", + "hire_date", + "id", + "job", + "last_name" + ] + }, + "PersonSummary": { + "type": "object", + "title": "Person Summary", + "description": "A summarized schema for a person, typically used in lists like direct reports where full details are not needed.", + "properties": { + "department_id": { + "type": "integer", + "format": "int64", + "description": "The ID of the department the person belongs to." + }, + "first_name": { + "type": "string", + "description": "The first name of the person." + }, + "hire_date": { + "type": "string", + "format": "date", + "description": "The hire date of the person (YYYY-MM-DD)." + }, + "id": { + "type": "integer", + "format": "int64", + "description": "The unique identifier for the person." + }, + "job_id": { + "type": "integer", + "format": "int64", + "description": "The ID of the job title held by the person." + }, + "last_name": { + "type": "string", + "description": "The last name of the person." + }, + "manager_id": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "The ID of the person's direct manager. Can be null." + } + }, + "required": [ + "department_id", + "first_name", + "hire_date", + "id", + "job_id", + "last_name" + ] + }, + "Department": { + "type": "object", + "title": "Department", + "description": "Schema for an organizational department.", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "The unique identifier for the department." + }, + "name": { + "type": "string", + "description": "The name of the department." + } + }, + "required": [ + "id", + "name" + ] + }, + "CreateDepartmentRequest": { + "type": "object", + "title": "Create Department Request", + "description": "Schema for creating a new department.", + "properties": { + "name": { + "type": "string", + "description": "The name of the new department.", + "example": "New Department" + } + }, + "required": [ + "name" + ] + }, + "Job": { + "type": "object", + "title": "Job", + "description": "Schema for a job title/role.", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "The unique identifier for the job title." + }, + "title": { + "type": "string", + "description": "The title of the job." + } + }, + "required": [ + "id", + "title" + ] + }, + "CreateJobRequest": { + "type": "object", + "title": "Create Job Request", + "description": "Schema for creating a new job title.", + "properties": { + "title": { + "type": "string", + "description": "The title of the new job.", + "example": "Lead Software Engineer" + } + }, + "required": [ + "title" + ] + } + }, + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT Authorization header using the Bearer scheme. Example: 'Authorization: Bearer {token}'" + } + } + } +} \ No newline at end of file diff --git a/scripts/create_db.sql b/scripts/create_db.sql index 9724d25..34d7ce8 100644 --- a/scripts/create_db.sql +++ b/scripts/create_db.sql @@ -1,29 +1,29 @@ CREATE TABLE job ( - id SERIAL PRIMARY KEY, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, title VARCHAR(50) UNIQUE NOT NULL -); +) ENGINE=InnoDB; CREATE TABLE department ( - id SERIAL PRIMARY KEY, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) UNIQUE NOT NULL -); +) ENGINE=InnoDB; CREATE TABLE person ( - id SERIAL PRIMARY KEY, - job_id int NOT NULL, - department_id int NOT NULL, - manager_id int NOT NULL, - first_name VARCHAR(50) UNIQUE NOT NULL, - last_name VARCHAR(50) UNIQUE NOT NULL, - hire_date DATE UNIQUE NOT NULL, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + job_id BIGINT UNSIGNED NOT NULL, + department_id BIGINT UNSIGNED NULL, + manager_id BIGINT UNSIGNED NULL, + first_name VARCHAR(50) NOT NULL, + last_name VARCHAR(50) NOT NULL, + hire_date DATE NOT NULL, UNIQUE (first_name, last_name), - CONSTRAINT fk_job FOREIGN KEY(job_id) REFERENCES job(id) ON DELETE SET NULL, - CONSTRAINT fk_department FOREIGN KEY(department_id) REFERENCES department(id) ON DELETE SET NULL, - CONSTRAINT fk_manager FOREIGN KEY(manager_id) REFERENCES person(id) ON DELETE SET NULL -); + CONSTRAINT fk_job FOREIGN KEY (job_id) REFERENCES job(id) ON DELETE RESTRICT, + CONSTRAINT fk_department FOREIGN KEY (department_id) REFERENCES department(id) ON DELETE SET NULL, + CONSTRAINT fk_manager FOREIGN KEY (manager_id) REFERENCES person(id) ON DELETE SET NULL +) ENGINE=InnoDB; CREATE TABLE users ( - id SERIAL PRIMARY KEY, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, - password VARCHAR UNIQUE NOT NULL -); + password VARCHAR(255) UNIQUE NOT NULL +) ENGINE=InnoDB; diff --git a/scripts/seed_db.sql b/scripts/seed_db.sql index 99ae703..5e84f3a 100644 --- a/scripts/seed_db.sql +++ b/scripts/seed_db.sql @@ -1,26 +1,40 @@ -INSERT INTO job(id, title) VALUES - (1, 'CEO'), - (2, 'M1'), - (3, 'E4'), - (4, 'E5'); +/* 99_seed_db.sql ─ place in ./scripts/ so it runs after the table-creation files */ -INSERT INTO department(id, name) VALUES - (1, 'Product'), - (2, 'Infrastructure'); +/* ---------- JOBS ---------- */ +INSERT INTO job (title) VALUES + ('CEO'), + ('M1'), + ('E4'), + ('E5'); -INSERT INTO person(id, job_id, department_id, manager_id, first_name, last_name, hire_date) VALUES - (1, 1, 1, 1, 'Sabryna', 'Peers', '2014-02-01'), - (2, 2, 1, 1, 'Tayler', 'Shantee', '2018-04-07'), - (3, 2, 1, 1, 'Madonna', 'Axl', '2018-03-08'), - (4, 4, 1, 2, 'Marcia', 'Stuart', '2020-01-11'), - (5, 3, 1, 2, 'Cliff', 'Rosalind', '2021-02-15'), - (6, 3, 1, 3, 'Lake', 'Philippa', '2022-05-21'), - (7, 3, 1, 3, 'Wynne', 'Walker', '2021-12-31'), - (8, 2, 2, 1, 'Sterling', 'Haley', '2019-11-02'), - (9, 2, 2, 8, 'Melissa', 'Garland', '2017-08-05'), - (10, 4, 2, 8, 'Leon', 'JayLee', '2022-02-17'), - (11, 4, 2, 8, 'Kaylie', 'Elyse', '2021-01-18'), - (12, 4, 2, 8, 'Yancey', 'Trenton', '2022-03-02'); +/* ---------- DEPARTMENTS ---------- */ +INSERT INTO department (name) VALUES + ('Product'), + ('Infrastructure'); -INSERT INTO users(id, username, password) VALUES - (1, 'admin', 'password'); +/* ---------- PEOPLE ---------- + Temporarily disable FK checks so manager_id can point to rows inserted later. +*/ +SET FOREIGN_KEY_CHECKS = 0; + +INSERT INTO person (id, job_id, department_id, manager_id, + first_name, last_name, hire_date) +VALUES + (1, 1, 1, 1, 'Sabryna', 'Peers', '2014-02-01'), + (2, 2, 1, 1, 'Tayler', 'Shantee', '2018-04-07'), + (3, 2, 1, 1, 'Madonna', 'Axl', '2018-03-08'), + (4, 4, 1, 2, 'Marcia', 'Stuart', '2020-01-11'), + (5, 3, 1, 2, 'Cliff', 'Rosalind', '2021-02-15'), + (6, 3, 1, 3, 'Lake', 'Philippa', '2022-05-21'), + (7, 3, 1, 3, 'Wynne', 'Walker', '2021-12-31'), + (8, 2, 2, 1, 'Sterling', 'Haley', '2019-11-02'), + (9, 2, 2, 8, 'Melissa', 'Garland', '2017-08-05'), + (10,4, 2, 8, 'Leon', 'JayLee', '2022-02-17'), + (11,4, 2, 8, 'Kaylie', 'Elyse', '2021-01-18'), + (12,4, 2, 8, 'Yancey', 'Trenton', '2022-03-02'); + +SET FOREIGN_KEY_CHECKS = 1; + +/* ---------- USERS ---------- */ +INSERT INTO users (username, `password`) +VALUES ('admin', '$2y$12$replace_this_with_your_real_hash'); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f4d3a4d..7483fe6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,8 +1,15 @@ cmake_minimum_required(VERSION 3.5) project(org_chart_test CXX) -add_executable(${PROJECT_NAME} test_main.cc test_controllers.cc) +add_executable(${PROJECT_NAME} job_test.cc ../models/Job.cc ../controllers/JobsController.cc ../models/Person.cc ../models/Department.cc ../utils/utils.cc) -target_link_libraries(${PROJECT_NAME} PRIVATE drogon) + +if (COVERAGE MATCHES "ON") # Or any build type you use for coverage + message(STATUS "Enabling code coverage flags for tests") + target_compile_options(${PROJECT_NAME} PRIVATE --coverage) + target_link_options(${PROJECT_NAME} PRIVATE --coverage) +endif() + +target_link_libraries(${PROJECT_NAME} PRIVATE drogon GTest::gtest GTest::gmock) ParseAndAddDrogonTests(${PROJECT_NAME}) diff --git a/test/job_test.cc b/test/job_test.cc new file mode 100644 index 0000000..c747bb8 --- /dev/null +++ b/test/job_test.cc @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include +#include +#include +#include "../models/Job.h" +#include "../controllers/JobsController.h" + +using namespace testing; +using namespace drogon; +using namespace drogon_model::org_chart; + +// Define the Callback type (assuming it expects a const HttpResponsePtr&) +using Callback = std::function; + +// Mock DbClient to simulate database operations +class MockDbClient : public drogon::orm::DbClient +{ +public: + MOCK_METHOD(std::shared_ptr, newTransaction, (const std::function &), (override)); + MOCK_METHOD(void, newTransactionAsync, (const std::function &)> &), (override)); + MOCK_METHOD(bool, hasAvailableConnections, (), (const, noexcept, override)); // <-- Add noexcept + MOCK_METHOD(void, setTimeout, (double timeout), (override)); + MOCK_METHOD(void, execSql, (const char *sql, size_t len, size_t timeout, std::vector &¶ms, std::vector &&types, std::vector &&lengths, ResultCallback &&callback, std::function &&errCallback), + (override)); +}; + +// Define the equality operator for Job class (if not already defined) +bool operator==(const Job &lhs, const Job &rhs) +{ + // Use public getters instead of direct member access + return lhs.getId() == rhs.getId() && lhs.getTitle() == rhs.getTitle(); // Add more fields as necessary +} + +// Mock the JobsController for testing +class JobsControllerTest : public ::testing::Test +{ +protected: + std::shared_ptr mockDbClient; + std::shared_ptr controller; + + void SetUp() override + { + mockDbClient = std::make_shared(); + controller = std::make_shared(mockDbClient); // Initialize with mock DbClient + } +}; + +// Test case to verify findAll behavior +TEST_F(JobsControllerTest, TestFindAll) +{ + auto req = HttpRequest::newHttpRequest(); + auto callback = [](const HttpResponsePtr &response) + { + EXPECT_TRUE(response != nullptr); + }; + + EXPECT_CALL(*mockDbClient, execSql(_, _, _, _, _, _, _, _)) + .WillOnce(Invoke([&](const char *, size_t, size_t, + std::vector &&, + std::vector &&, + std::vector &&, + ResultCallback &&, + std::function &&) + { + HttpResponsePtr mockResponse = HttpResponse::newHttpResponse(); + callback(mockResponse); + })); + + controller->get(req, callback); // Pass valid request and callback +} + +// Test case to verify insert behavior +TEST_F(JobsControllerTest, TestInsert) +{ + Job job; + job.setId(1); + job.setTitle("Software Engineer"); + + auto req = HttpRequest::newHttpRequest(); + auto callback = [](const HttpResponsePtr &response) + { + EXPECT_TRUE(response != nullptr); + }; + + EXPECT_CALL(*mockDbClient, execSql(_, _, _, _, _, _, _, _)) + .WillOnce(Invoke([&](const char *, size_t, size_t, + std::vector &&, + std::vector &&, + std::vector &&, + ResultCallback &&, + std::function &&) + { + HttpResponsePtr mockResponse = HttpResponse::newHttpResponse(); + callback(mockResponse); + })); + + controller->createOne(req, callback, std::move(job)); // Pass valid request and callback +} + +// Main test entry point +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/third_party/gmock b/third_party/gmock new file mode 160000 index 0000000..245b237 --- /dev/null +++ b/third_party/gmock @@ -0,0 +1 @@ +Subproject commit 245b237927e4d3711f4193432bfd830b29b28346 diff --git a/third_party/gtest b/third_party/gtest new file mode 160000 index 0000000..309dab8 --- /dev/null +++ b/third_party/gtest @@ -0,0 +1 @@ +Subproject commit 309dab8d4bbfcef0ef428762c6fec7172749de0f diff --git a/utils/utils.h b/utils/utils.h index 4f76fae..506ae36 100644 --- a/utils/utils.h +++ b/utils/utils.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include void badRequest ( std::function &&callback, @@ -9,3 +11,47 @@ void badRequest ( ); Json::Value makeErrResp(std::string err); + + +inline bool rowExists(const drogon::orm::DbClientPtr& db, + const std::string& table, + uint64_t id) +{ + try + { + // The sync API is simplest here + drogon::orm::Result r = + db->execSqlSync("SELECT 1 FROM " + table + " WHERE id = ?", id); + + return !r.empty(); + } + catch (const drogon::orm::DrogonDbException&) + { + // On any SQL error treat it as β€œdoesn’t exist” + return false; + } +} + + +inline bool personNameExists(const drogon::orm::DbClientPtr& db, + const std::string& first, + const std::string& last, + int64_t excludeId = -1) +{ + std::string sql = + "SELECT 1 FROM person WHERE first_name = ? AND last_name = ?"; + if (excludeId >= 0) + sql += " AND id <> ?"; + + try { + auto r = excludeId < 0 + ? db->execSqlSync(sql, first, last) + : db->execSqlSync(sql, first, last, excludeId); + return !r.empty(); + } + catch (const drogon::orm::DrogonDbException&) { + /* On any DB error, be conservative – say it exists so we bail out + before MySQL can throw 1062 later. */ + return true; + } +} \ No newline at end of file