~aleteoryx/muditaos

3ac4bd4935e598da305b77e7dea8af20413ba668 — Wojtek Rzepecki 4 years ago 4ec11ba
[EGD-7127] Store imported contacts in DB

Added API for storing imported contacts list
in phonebook database
M module-db/CMakeLists.txt => module-db/CMakeLists.txt +2 -0
@@ 102,6 102,8 @@ set(SOURCES
        queries/phonebook/QueryContactUpdate.cpp
        queries/phonebook/QueryContactRemove.cpp
        queries/phonebook/QueryNumberGetByID.cpp
        queries/phonebook/QueryMergeContactsList.cpp
        queries/phonebook/QueryCheckContactsListDuplicates.cpp
        queries/alarms/QueryAlarmsAdd.cpp
        queries/alarms/QueryAlarmsRemove.cpp
        queries/alarms/QueryAlarmsEdit.cpp

M module-db/Interface/ContactRecord.cpp => module-db/Interface/ContactRecord.cpp +77 -0
@@ 6,6 6,8 @@
#include "queries/phonebook/QueryContactGetByID.hpp"
#include "queries/phonebook/QueryContactUpdate.hpp"
#include "queries/phonebook/QueryContactRemove.hpp"
#include "queries/phonebook/QueryMergeContactsList.hpp"
#include "queries/phonebook/QueryCheckContactsListDuplicates.hpp"
#include <Utils.hpp>

#include <queries/phonebook/QueryContactGet.hpp>


@@ 159,6 161,12 @@ auto ContactRecordInterface::runQuery(std::shared_ptr<db::Query> query) -> std::
    else if (typeid(*query) == typeid(db::query::NumberGetByID)) {
        return numberGetByIdQuery(query);
    }
    else if (typeid(*query) == typeid(db::query::MergeContactsList)) {
        return mergeContactsListQuery(query);
    }
    else if (typeid(*query) == typeid(db::query::CheckContactsListDuplicates)) {
        return checkContactsListDuplicatesQuery(query);
    }
    else {
        error_db_data("Unexpected query type.");
        return nullptr;


@@ 357,6 365,26 @@ auto ContactRecordInterface::numberGetByIdQuery(std::shared_ptr<db::Query> query
    return response;
}

auto ContactRecordInterface::mergeContactsListQuery(std::shared_ptr<db::Query> query)
    -> std::unique_ptr<db::QueryResult>
{
    auto mergeQuery = static_cast<db::query::MergeContactsList *>(query.get());
    auto ret        = ContactRecordInterface::MergeContactsList(mergeQuery->getContactsList());
    auto response   = std::make_unique<db::query::MergeContactsListResult>(ret);
    response->setRequestQuery(query);
    return response;
}

auto ContactRecordInterface::checkContactsListDuplicatesQuery(std::shared_ptr<db::Query> query)
    -> std::unique_ptr<db::QueryResult>
{
    auto mergeQuery = static_cast<db::query::CheckContactsListDuplicates *>(query.get());
    auto response   = std::make_unique<db::query::CheckContactsListDuplicatesResult>(
        std::move(ContactRecordInterface::CheckContactsListDuplicates(mergeQuery->getContactsList())));
    response->setRequestQuery(query);
    return response;
}

auto ContactRecordInterface::splitNumberIDs(const std::string &numberIDs) -> std::vector<std::uint32_t>
{
    std::stringstream source(numberIDs);


@@ 1172,3 1200,52 @@ auto ContactRecordInterface::GetNumbersIdsByContact(std::uint32_t contactId) -> 
    }
    return numbersIds;
}

auto ContactRecordInterface::MergeContactsList(std::vector<ContactRecord> &contacts) -> bool
{
    std::vector<ContactNumberHolder> contactNumberHolders;
    auto numberMatcher = buildNumberMatcher(contactNumberHolders);

    for (auto &contact : contacts) {
        // Important: Comparing only single number contacts
        if (contact.numbers.size() > 1) {
            LOG_WARN("Contact with multiple numbers detected - ignoring all numbers except first");
        }
        auto matchedNumber = numberMatcher.bestMatch(contact.numbers[0].number, utils::PhoneNumber::Match::POSSIBLE);

        if (matchedNumber == numberMatcher.END) {
            if (!Add(contact)) {
                LOG_ERROR("Contacts list merge fail when adding the contact.");
                return false;
            }
        }
        else {
            // Complete override of the contact data
            contact.ID = matchedNumber->getContactID();
            Update(contact);
            // Rebuild number matcher
            numberMatcher = buildNumberMatcher(contactNumberHolders);
        }
    }
    return true;
}

auto ContactRecordInterface::CheckContactsListDuplicates(std::vector<ContactRecord> &contacts)
    -> std::vector<ContactRecord>
{
    std::vector<ContactRecord> duplicates;
    std::vector<ContactNumberHolder> contactNumberHolders;
    auto numberMatcher = buildNumberMatcher(contactNumberHolders);

    for (auto &contact : contacts) {
        // Important: Comparing only single number contacts
        if (contact.numbers.size() > 1) {
            LOG_WARN("Contact with multiple numbers detected - ignoring all numbers except first");
        }
        auto matchedNumber = numberMatcher.bestMatch(contact.numbers[0].number, utils::PhoneNumber::Match::POSSIBLE);
        if (matchedNumber != numberMatcher.END) {
            duplicates.push_back(contact);
        }
    }
    return duplicates;
}

M module-db/Interface/ContactRecord.hpp => module-db/Interface/ContactRecord.hpp +18 -0
@@ 220,6 220,22 @@ class ContactRecordInterface : public RecordInterface<ContactRecord, ContactReco

    auto GetNumbersIdsByContact(std::uint32_t contactId) -> std::vector<std::uint32_t>;

    /**
     * @brief Merge contacts list with overriding the duplicates in contacts DB
     *
     * @param contacts vector of contacts with single number
     * @return boolean status
     */
    auto MergeContactsList(std::vector<ContactRecord> &contacts) -> bool;

    /**
     * @brief Check which contacts in vector are duplicating contacts in DB
     *
     * @param contacts vector of contacts with single number
     * @return vector of contacts with numbers appearing in contacts DB
     */
    auto CheckContactsListDuplicates(std::vector<ContactRecord> &contacts) -> std::vector<ContactRecord>;

  private:
    ContactsDB *contactDB;



@@ 248,4 264,6 @@ class ContactRecordInterface : public RecordInterface<ContactRecord, ContactReco
    auto updateQuery(std::shared_ptr<db::Query> query) -> std::unique_ptr<db::QueryResult>;
    auto removeQuery(std::shared_ptr<db::Query> query) -> std::unique_ptr<db::QueryResult>;
    auto numberGetByIdQuery(std::shared_ptr<db::Query> query) -> std::unique_ptr<db::QueryResult>;
    auto mergeContactsListQuery(std::shared_ptr<db::Query> query) -> std::unique_ptr<db::QueryResult>;
    auto checkContactsListDuplicatesQuery(std::shared_ptr<db::Query> query) -> std::unique_ptr<db::QueryResult>;
};

A module-db/doc/contacts_import.md => module-db/doc/contacts_import.md +3 -0
@@ 0,0 1,3 @@
## Sequence of contacts import to contacts DB

![](contacts_import.svg)
\ No newline at end of file

A module-db/doc/contacts_import.puml => module-db/doc/contacts_import.puml +30 -0
@@ 0,0 1,30 @@
@startuml

participant "Application" as app
participant "ContactRecord" as rec
participant "contacts DB" as db

== Checking for duplicates ==

app -> rec : contacts to check\n(query::CheckContactsListDuplicates)
rec -> db : get numbers
db -> rec
rec -> rec : numbers comparison
rec -> app : duplicated contacts

== Merging contacts list to DB ==

app -> rec : contacts to merge\n(query::MergeContactsList)
rec -> db : get numbers
db -> rec
rec -> rec : numbers comparison

group number not found
    rec -> db : Add contact
end
group number found in db
    rec -> db : Update contact by overriding old data
end
rec -> app : status response

@enduml
\ No newline at end of file

A module-db/doc/contacts_import.svg => module-db/doc/contacts_import.svg +40 -0
@@ 0,0 1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="627px" preserveAspectRatio="none" style="width:658px;height:627px;background:#FFFFFF;" version="1.1" viewBox="0 0 658 627" width="658px" zoomAndPan="magnify"><defs><filter height="300%" id="f12hef58xpqd4m" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><rect fill="#FFFFFF" filter="url(#f12hef58xpqd4m)" height="46.2656" style="stroke:#000000;stroke-width:2.0;" width="397.5" x="243.5" y="431.7578"/><rect fill="#FFFFFF" filter="url(#f12hef58xpqd4m)" height="46.2656" style="stroke:#000000;stroke-width:2.0;" width="397.5" x="243.5" y="492.0234"/><line style="stroke:#A80036;stroke-width:1.0;stroke-dasharray:5.0,5.0;" x1="51" x2="51" y1="40.2969" y2="584.4219"/><line style="stroke:#A80036;stroke-width:1.0;stroke-dasharray:5.0,5.0;" x1="314.5" x2="314.5" y1="40.2969" y2="584.4219"/><line style="stroke:#A80036;stroke-width:1.0;stroke-dasharray:5.0,5.0;" x1="579" x2="579" y1="40.2969" y2="584.4219"/><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="89" x="5" y="5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="75" x="12" y="24.9951">Application</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="89" x="5" y="583.4219"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="75" x="12" y="603.417">Application</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="118" x="253.5" y="5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="104" x="260.5" y="24.9951">ContactRecord</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="118" x="253.5" y="583.4219"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="104" x="260.5" y="603.417">ContactRecord</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="99" x="528" y="5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="85" x="535" y="24.9951">contacts DB</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="99" x="528" y="583.4219"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="85" x="535" y="603.417">contacts DB</text><rect fill="#EEEEEE" filter="url(#f12hef58xpqd4m)" height="3" style="stroke:#EEEEEE;stroke-width:1.0;" width="651" x="0" y="70.8633"/><line style="stroke:#000000;stroke-width:1.0;" x1="0" x2="651" y1="70.8633" y2="70.8633"/><line style="stroke:#000000;stroke-width:1.0;" x1="0" x2="651" y1="73.8633" y2="73.8633"/><rect fill="#EEEEEE" filter="url(#f12hef58xpqd4m)" height="23.1328" style="stroke:#000000;stroke-width:2.0;" width="195" x="228" y="60.2969"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacing" textLength="176" x="234" y="76.3638">Checking for duplicates</text><polygon fill="#A80036" points="302.5,125.6953,312.5,129.6953,302.5,133.6953,306.5,129.6953" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="51.5" x2="308.5" y1="129.6953" y2="129.6953"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="113" x="58.5" y="109.4966">contacts to check</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="239" x="58.5" y="124.6294">(query::CheckContactsListDuplicates)</text><polygon fill="#A80036" points="567.5,154.8281,577.5,158.8281,567.5,162.8281,571.5,158.8281" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="573.5" y1="158.8281" y2="158.8281"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="82" x="321.5" y="153.7622">get numbers</text><polygon fill="#A80036" points="325.5,168.8281,315.5,172.8281,325.5,176.8281,321.5,172.8281" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="319.5" x2="578.5" y1="172.8281" y2="172.8281"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="356.5" y1="201.9609" y2="201.9609"/><line style="stroke:#A80036;stroke-width:1.0;" x1="356.5" x2="356.5" y1="201.9609" y2="214.9609"/><line style="stroke:#A80036;stroke-width:1.0;" x1="315.5" x2="356.5" y1="214.9609" y2="214.9609"/><polygon fill="#A80036" points="325.5,210.9609,315.5,214.9609,325.5,218.9609,321.5,214.9609" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="136" x="321.5" y="196.895">numbers comparison</text><polygon fill="#A80036" points="62.5,240.0938,52.5,244.0938,62.5,248.0938,58.5,244.0938" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="56.5" x2="313.5" y1="244.0938" y2="244.0938"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="125" x="68.5" y="239.0278">duplicated contacts</text><rect fill="#EEEEEE" filter="url(#f12hef58xpqd4m)" height="3" style="stroke:#EEEEEE;stroke-width:1.0;" width="651" x="0" y="272.6602"/><line style="stroke:#000000;stroke-width:1.0;" x1="0" x2="651" y1="272.6602" y2="272.6602"/><line style="stroke:#000000;stroke-width:1.0;" x1="0" x2="651" y1="275.6602" y2="275.6602"/><rect fill="#EEEEEE" filter="url(#f12hef58xpqd4m)" height="23.1328" style="stroke:#000000;stroke-width:2.0;" width="221" x="215" y="262.0938"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacing" textLength="202" x="221" y="278.1606">Merging contacts list to DB</text><polygon fill="#A80036" points="302.5,327.4922,312.5,331.4922,302.5,335.4922,306.5,331.4922" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="51.5" x2="308.5" y1="331.4922" y2="331.4922"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="118" x="58.5" y="311.2935">contacts to merge</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="173" x="58.5" y="326.4263">(query::MergeContactsList)</text><polygon fill="#A80036" points="567.5,356.625,577.5,360.625,567.5,364.625,571.5,360.625" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="573.5" y1="360.625" y2="360.625"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="82" x="321.5" y="355.5591">get numbers</text><polygon fill="#A80036" points="325.5,370.625,315.5,374.625,325.5,378.625,321.5,374.625" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="319.5" x2="578.5" y1="374.625" y2="374.625"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="356.5" y1="403.7578" y2="403.7578"/><line style="stroke:#A80036;stroke-width:1.0;" x1="356.5" x2="356.5" y1="403.7578" y2="416.7578"/><line style="stroke:#A80036;stroke-width:1.0;" x1="315.5" x2="356.5" y1="416.7578" y2="416.7578"/><polygon fill="#A80036" points="325.5,412.7578,315.5,416.7578,325.5,420.7578,321.5,416.7578" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="136" x="321.5" y="398.6919">numbers comparison</text><path d="M243.5,431.7578 L422.5,431.7578 L422.5,438.7578 L412.5,448.7578 L243.5,448.7578 L243.5,431.7578 " fill="#EEEEEE" style="stroke:#000000;stroke-width:1.0;"/><rect fill="none" height="46.2656" style="stroke:#000000;stroke-width:2.0;" width="397.5" x="243.5" y="431.7578"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacing" textLength="134" x="258.5" y="444.8247">number not found</text><polygon fill="#A80036" points="567.5,466.0234,577.5,470.0234,567.5,474.0234,571.5,470.0234" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="573.5" y1="470.0234" y2="470.0234"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="77" x="321.5" y="464.9575">Add contact</text><path d="M243.5,492.0234 L436.5,492.0234 L436.5,499.0234 L426.5,509.0234 L243.5,509.0234 L243.5,492.0234 " fill="#EEEEEE" style="stroke:#000000;stroke-width:1.0;"/><rect fill="none" height="46.2656" style="stroke:#000000;stroke-width:2.0;" width="397.5" x="243.5" y="492.0234"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacing" textLength="148" x="258.5" y="505.0903">number found in db</text><polygon fill="#A80036" points="567.5,526.2891,577.5,530.2891,567.5,534.2891,571.5,530.2891" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="573.5" y1="530.2891" y2="530.2891"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="241" x="321.5" y="525.2231">Update contact by overriding old data</text><polygon fill="#A80036" points="62.5,562.4219,52.5,566.4219,62.5,570.4219,58.5,566.4219" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="56.5" x2="313.5" y1="566.4219" y2="566.4219"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="103" x="68.5" y="561.356">status response</text><!--MD5=[921bcca0682cb10fe9b051cd54a58a3c]
@startuml

participant "Application" as app
participant "ContactRecord" as rec
participant "contacts DB" as db

== Checking for duplicates ==

app -> rec : contacts to check\n(query::CheckContactsListDuplicates)
rec -> db : get numbers
db -> rec
rec -> rec : numbers comparison
rec -> app : duplicated contacts

== Merging contacts list to DB ==

app -> rec : contacts to merge\n(query::MergeContactsList)
rec -> db : get numbers
db -> rec
rec -> rec : numbers comparison

group number not found
    rec -> db : Add contact
end
group number found in db
    rec -> db : Update contact by overriding old data
end
rec -> app : status response

@enduml

PlantUML version 1.2021.7(Sun May 23 14:40:07 CEST 2021)
(GPL source distribution)
Java Runtime: OpenJDK Runtime Environment
JVM: OpenJDK 64-Bit Server VM
Default Encoding: UTF-8
Language: pl
Country: PL
--></g></svg>
\ No newline at end of file

A module-db/queries/phonebook/QueryCheckContactsListDuplicates.cpp => module-db/queries/phonebook/QueryCheckContactsListDuplicates.cpp +35 -0
@@ 0,0 1,35 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "QueryCheckContactsListDuplicates.hpp"
#include <string>

using namespace db::query;

CheckContactsListDuplicates::CheckContactsListDuplicates(std::vector<ContactRecord> contacts)
    : Query(Query::Type::Read), contacts(std::move(contacts))
{}

std::vector<ContactRecord> &CheckContactsListDuplicates::getContactsList()
{
    return contacts;
}

[[nodiscard]] auto CheckContactsListDuplicates::debugInfo() const -> std::string
{
    return "CheckContactsListDuplicates";
}

CheckContactsListDuplicatesResult::CheckContactsListDuplicatesResult(std::vector<ContactRecord> duplicates)
    : duplicates(std::move(duplicates))
{}

std::vector<ContactRecord> &CheckContactsListDuplicatesResult::getDuplicates()
{
    return duplicates;
}

[[nodiscard]] auto CheckContactsListDuplicatesResult::debugInfo() const -> std::string
{
    return "CheckContactsListDuplicatesResult";
}

A module-db/queries/phonebook/QueryCheckContactsListDuplicates.hpp => module-db/queries/phonebook/QueryCheckContactsListDuplicates.hpp +40 -0
@@ 0,0 1,40 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "Common/Query.hpp"
#include <queries/RecordQuery.hpp>
#include <queries/Filter.hpp>
#include <Interface/ContactRecord.hpp>

#include <string>

namespace db::query
{

    class CheckContactsListDuplicates : public Query
    {
      public:
        CheckContactsListDuplicates(std::vector<ContactRecord> contacts);
        [[nodiscard]] auto debugInfo() const -> std::string override;

        std::vector<ContactRecord> &getContactsList();

      private:
        std::vector<ContactRecord> contacts;
    };

    class CheckContactsListDuplicatesResult : public QueryResult
    {
      public:
        CheckContactsListDuplicatesResult(std::vector<ContactRecord> duplicates);
        std::vector<ContactRecord> &getDuplicates();

        [[nodiscard]] auto debugInfo() const -> std::string override;

      private:
        std::vector<ContactRecord> duplicates;
    };

}; // namespace db::query

A module-db/queries/phonebook/QueryMergeContactsList.cpp => module-db/queries/phonebook/QueryMergeContactsList.cpp +29 -0
@@ 0,0 1,29 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "QueryMergeContactsList.hpp"
#include <string>

using namespace db::query;

MergeContactsList::MergeContactsList(std::vector<ContactRecord> contacts)
    : Query(Query::Type::Read), contacts(std::move(contacts))
{}

std::vector<ContactRecord> &MergeContactsList::getContactsList()
{
    return contacts;
}

MergeContactsListResult::MergeContactsListResult(bool result) : result(result)
{}

[[nodiscard]] auto MergeContactsList::debugInfo() const -> std::string
{
    return "MergeContactsList";
}

[[nodiscard]] auto MergeContactsListResult::debugInfo() const -> std::string
{
    return "MergeContactsListResult";
}

A module-db/queries/phonebook/QueryMergeContactsList.hpp => module-db/queries/phonebook/QueryMergeContactsList.hpp +42 -0
@@ 0,0 1,42 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "Common/Query.hpp"
#include <queries/RecordQuery.hpp>
#include <queries/Filter.hpp>
#include <Interface/ContactRecord.hpp>

#include <string>

namespace db::query
{

    class MergeContactsList : public Query
    {
      public:
        MergeContactsList(std::vector<ContactRecord> contacts);
        [[nodiscard]] auto debugInfo() const -> std::string override;

        std::vector<ContactRecord> &getContactsList();

      private:
        std::vector<ContactRecord> contacts;
    };

    class MergeContactsListResult : public QueryResult
    {
      public:
        MergeContactsListResult(bool result);
        [[nodiscard]] auto getResult() const noexcept -> bool
        {
            return result;
        }
        [[nodiscard]] auto debugInfo() const -> std::string override;

      private:
        bool result = false;
    };

}; // namespace db::query

M module-db/tests/ContactsRecord_tests.cpp => module-db/tests/ContactsRecord_tests.cpp +186 -0
@@ 396,3 396,189 @@ TEST_CASE("Contact record numbers update")

    Database::deinitialize();
}

TEST_CASE("Contacts list merge")
{
    Database::initialize();
    const auto contactsPath = (std::filesystem::path{"sys/user"} / "contacts.db");
    RemoveDbFiles(contactsPath.stem());

    ContactsDB contactDB(contactsPath.c_str());
    REQUIRE(contactDB.isInitialized());

    // Preparation of DB initial state
    auto records                                                          = ContactRecordInterface(&contactDB);
    std::array<std::pair<std::string, std::string>, 3> rawContactsInitial = {
        {{"600100100", "test1"}, {"600100200", "test2"}, {"600100300", "test3"}}};
    for (auto &rawContact : rawContactsInitial) {
        ContactRecord record;
        record.primaryName = rawContact.second;
        record.numbers = std::vector<ContactRecord::Number>({ContactRecord::Number(rawContact.first, std::string(""))});
        REQUIRE(records.Add(record));
    }

    SECTION("Merge contacts without overlaps")
    {
        std::array<std::pair<std::string, std::string>, 3> rawContactsToAdd = {
            {{"600100400", "test4"}, {"600100500", "test5"}, {"600100600", "test6"}}};

        // Prepare contacts list to merge
        std::vector<ContactRecord> contacts;
        for (auto &rawContact : rawContactsToAdd) {
            ContactRecord record;
            record.primaryName = rawContact.second;
            record.numbers =
                std::vector<ContactRecord::Number>({ContactRecord::Number(rawContact.first, std::string(""))});
            contacts.push_back(record);
        }
        REQUIRE(records.MergeContactsList(contacts));

        // Validate if non-overlapping were appended to DB
        REQUIRE(records.GetCount() == (rawContactsInitial.size() + rawContactsToAdd.size()));

        auto validatationRecord = records.GetByID(1);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[0].first);
        REQUIRE(validatationRecord.primaryName == rawContactsInitial[0].second);
        validatationRecord = records.GetByID(2);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[1].first);
        REQUIRE(validatationRecord.primaryName == rawContactsInitial[1].second);
        validatationRecord = records.GetByID(3);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[2].first);
        REQUIRE(validatationRecord.primaryName == rawContactsInitial[2].second);
        validatationRecord = records.GetByID(4);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsToAdd[0].first);
        REQUIRE(validatationRecord.primaryName == rawContactsToAdd[0].second);
        validatationRecord = records.GetByID(5);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsToAdd[1].first);
        REQUIRE(validatationRecord.primaryName == rawContactsToAdd[1].second);
        validatationRecord = records.GetByID(6);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsToAdd[2].first);
        REQUIRE(validatationRecord.primaryName == rawContactsToAdd[2].second);
    }

    SECTION("Merge contacts with numbers overlaps")
    {
        REQUIRE(records.GetCount() == rawContactsInitial.size());

        std::array<std::pair<std::string, std::string>, 3> rawContactsOverlapping = {
            {{rawContactsInitial[1].first, "test7"}, {"600100800", "test8"}, {rawContactsInitial[0].first, "test9"}}};
        constexpr auto numberOfNewContacts = 1;

        // Prepare contacts list to merge
        std::vector<ContactRecord> contacts;
        for (auto &rawContact : rawContactsOverlapping) {
            ContactRecord record;
            record.primaryName = rawContact.second;
            record.numbers =
                std::vector<ContactRecord::Number>({ContactRecord::Number(rawContact.first, std::string(""))});
            contacts.push_back(record);
        }
        REQUIRE(records.MergeContactsList(contacts));

        REQUIRE(records.GetCount() == (rawContactsInitial.size() + numberOfNewContacts));

        // Overlapping contacts replaced with same ID
        auto validatationRecord = records.GetByID(1);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[0].first);
        REQUIRE(validatationRecord.primaryName == rawContactsOverlapping[2].second);
        validatationRecord = records.GetByID(2);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[1].first);
        REQUIRE(validatationRecord.primaryName == rawContactsOverlapping[0].second);
        // Non-overlapping contact left untouched
        validatationRecord = records.GetByID(3);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[2].first);
        REQUIRE(validatationRecord.primaryName == rawContactsInitial[2].second);
        // Non-overlapping new contact added
        validatationRecord = records.GetByID(4);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsOverlapping[1].first);
        REQUIRE(validatationRecord.primaryName == rawContactsOverlapping[1].second);
    }

    Database::deinitialize();
}

TEST_CASE("Contacts list merge - advanced cases")
{
    Database::initialize();
    const auto contactsPath = (std::filesystem::path{"sys/user"} / "contacts.db");
    RemoveDbFiles(contactsPath.stem());

    ContactsDB contactDB(contactsPath.c_str());
    REQUIRE(contactDB.isInitialized());

    // Preparation of DB initial state
    auto records = ContactRecordInterface(&contactDB);
    // 3 numbers in single contact
    std::array<std::string, 3> numbers = {"600100100", "600100200", "600100300"};
    ContactRecord record;
    record.primaryName = "test";
    for (auto &number : numbers) {
        record.numbers.push_back({ContactRecord::Number(number, std::string(""))});
    }
    REQUIRE(records.Add(record));

    SECTION("Compared number is secondary number")
    {
        std::pair<std::string, std::string> rawContact = {"600100200", "test2"};
        std::vector<ContactRecord> contacts;

        // Prepare contacts list to merge
        ContactRecord record;
        record.primaryName = rawContact.second;
        record.numbers = std::vector<ContactRecord::Number>({ContactRecord::Number(rawContact.first, std::string(""))});
        contacts.push_back(record);
        REQUIRE(records.MergeContactsList(contacts));

        REQUIRE(records.GetCount() == 1);

        // First contact replaced
        auto validatationRecord = records.GetByID(1);
        REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContact.first);
        REQUIRE(validatationRecord.primaryName == rawContact.second);
    }
}

TEST_CASE("Contacts list duplicates search")
{
    Database::initialize();
    const auto contactsPath = (std::filesystem::path{"sys/user"} / "contacts.db");
    RemoveDbFiles(contactsPath.stem());

    ContactsDB contactDB(contactsPath.c_str());
    REQUIRE(contactDB.isInitialized());

    // Preparation of DB initial state
    auto records                                                          = ContactRecordInterface(&contactDB);
    std::array<std::pair<std::string, std::string>, 3> rawContactsInitial = {
        {{"600100100", "test1"}, {"600100200", "test2"}, {"600100300", "test3"}}};
    for (auto &rawContact : rawContactsInitial) {
        ContactRecord record;
        record.primaryName = rawContact.second;
        record.numbers = std::vector<ContactRecord::Number>({ContactRecord::Number(rawContact.first, std::string(""))});
        REQUIRE(records.Add(record));
    }

    // Prepare contacts list to compare with DB
    std::array<std::pair<std::string, std::string>, 3> rawContactsToCheck = {
        {rawContactsInitial[2], {"600100500", "test5"}, rawContactsInitial[0]}};
    constexpr auto numOfDuplicatedContacts = 2;

    std::vector<ContactRecord> contacts;
    for (auto &rawContact : rawContactsToCheck) {
        ContactRecord record;
        record.primaryName = rawContact.second;
        record.numbers = std::vector<ContactRecord::Number>({ContactRecord::Number(rawContact.first, std::string(""))});
        contacts.push_back(record);
    }
    auto duplicates = records.CheckContactsListDuplicates(contacts);

    REQUIRE(duplicates.size() == numOfDuplicatedContacts);

    REQUIRE(duplicates[0].numbers[0].number.getEntered() == rawContactsToCheck[0].first);
    REQUIRE(duplicates[0].primaryName == rawContactsToCheck[0].second);

    REQUIRE(duplicates[1].numbers[0].number.getEntered() == rawContactsToCheck[2].first);
    REQUIRE(duplicates[1].primaryName == rawContactsToCheck[2].second);

    Database::deinitialize();
}

A out/module-db/doc/contacts_import/contacts_import.svg => out/module-db/doc/contacts_import/contacts_import.svg +40 -0
@@ 0,0 1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="627px" preserveAspectRatio="none" style="width:658px;height:627px;background:#FFFFFF;" version="1.1" viewBox="0 0 658 627" width="658px" zoomAndPan="magnify"><defs><filter height="300%" id="f12hef58xpqd4m" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><rect fill="#FFFFFF" filter="url(#f12hef58xpqd4m)" height="46.2656" style="stroke:#000000;stroke-width:2.0;" width="397.5" x="243.5" y="431.7578"/><rect fill="#FFFFFF" filter="url(#f12hef58xpqd4m)" height="46.2656" style="stroke:#000000;stroke-width:2.0;" width="397.5" x="243.5" y="492.0234"/><line style="stroke:#A80036;stroke-width:1.0;stroke-dasharray:5.0,5.0;" x1="51" x2="51" y1="40.2969" y2="584.4219"/><line style="stroke:#A80036;stroke-width:1.0;stroke-dasharray:5.0,5.0;" x1="314.5" x2="314.5" y1="40.2969" y2="584.4219"/><line style="stroke:#A80036;stroke-width:1.0;stroke-dasharray:5.0,5.0;" x1="579" x2="579" y1="40.2969" y2="584.4219"/><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="89" x="5" y="5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="75" x="12" y="24.9951">Application</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="89" x="5" y="583.4219"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="75" x="12" y="603.417">Application</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="118" x="253.5" y="5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="104" x="260.5" y="24.9951">ContactRecord</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="118" x="253.5" y="583.4219"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="104" x="260.5" y="603.417">ContactRecord</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="99" x="528" y="5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="85" x="535" y="24.9951">contacts DB</text><rect fill="#FEFECE" filter="url(#f12hef58xpqd4m)" height="30.2969" style="stroke:#A80036;stroke-width:1.5;" width="99" x="528" y="583.4219"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="85" x="535" y="603.417">contacts DB</text><rect fill="#EEEEEE" filter="url(#f12hef58xpqd4m)" height="3" style="stroke:#EEEEEE;stroke-width:1.0;" width="651" x="0" y="70.8633"/><line style="stroke:#000000;stroke-width:1.0;" x1="0" x2="651" y1="70.8633" y2="70.8633"/><line style="stroke:#000000;stroke-width:1.0;" x1="0" x2="651" y1="73.8633" y2="73.8633"/><rect fill="#EEEEEE" filter="url(#f12hef58xpqd4m)" height="23.1328" style="stroke:#000000;stroke-width:2.0;" width="195" x="228" y="60.2969"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacing" textLength="176" x="234" y="76.3638">Checking for duplicates</text><polygon fill="#A80036" points="302.5,125.6953,312.5,129.6953,302.5,133.6953,306.5,129.6953" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="51.5" x2="308.5" y1="129.6953" y2="129.6953"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="113" x="58.5" y="109.4966">contacts to check</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="239" x="58.5" y="124.6294">(query::CheckContactsListDuplicates)</text><polygon fill="#A80036" points="567.5,154.8281,577.5,158.8281,567.5,162.8281,571.5,158.8281" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="573.5" y1="158.8281" y2="158.8281"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="82" x="321.5" y="153.7622">get numbers</text><polygon fill="#A80036" points="325.5,168.8281,315.5,172.8281,325.5,176.8281,321.5,172.8281" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="319.5" x2="578.5" y1="172.8281" y2="172.8281"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="356.5" y1="201.9609" y2="201.9609"/><line style="stroke:#A80036;stroke-width:1.0;" x1="356.5" x2="356.5" y1="201.9609" y2="214.9609"/><line style="stroke:#A80036;stroke-width:1.0;" x1="315.5" x2="356.5" y1="214.9609" y2="214.9609"/><polygon fill="#A80036" points="325.5,210.9609,315.5,214.9609,325.5,218.9609,321.5,214.9609" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="136" x="321.5" y="196.895">numbers comparison</text><polygon fill="#A80036" points="62.5,240.0938,52.5,244.0938,62.5,248.0938,58.5,244.0938" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="56.5" x2="313.5" y1="244.0938" y2="244.0938"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="125" x="68.5" y="239.0278">duplicated contacts</text><rect fill="#EEEEEE" filter="url(#f12hef58xpqd4m)" height="3" style="stroke:#EEEEEE;stroke-width:1.0;" width="651" x="0" y="272.6602"/><line style="stroke:#000000;stroke-width:1.0;" x1="0" x2="651" y1="272.6602" y2="272.6602"/><line style="stroke:#000000;stroke-width:1.0;" x1="0" x2="651" y1="275.6602" y2="275.6602"/><rect fill="#EEEEEE" filter="url(#f12hef58xpqd4m)" height="23.1328" style="stroke:#000000;stroke-width:2.0;" width="221" x="215" y="262.0938"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacing" textLength="202" x="221" y="278.1606">Merging contacts list to DB</text><polygon fill="#A80036" points="302.5,327.4922,312.5,331.4922,302.5,335.4922,306.5,331.4922" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="51.5" x2="308.5" y1="331.4922" y2="331.4922"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="118" x="58.5" y="311.2935">contacts to merge</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="173" x="58.5" y="326.4263">(query::MergeContactsList)</text><polygon fill="#A80036" points="567.5,356.625,577.5,360.625,567.5,364.625,571.5,360.625" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="573.5" y1="360.625" y2="360.625"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="82" x="321.5" y="355.5591">get numbers</text><polygon fill="#A80036" points="325.5,370.625,315.5,374.625,325.5,378.625,321.5,374.625" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="319.5" x2="578.5" y1="374.625" y2="374.625"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="356.5" y1="403.7578" y2="403.7578"/><line style="stroke:#A80036;stroke-width:1.0;" x1="356.5" x2="356.5" y1="403.7578" y2="416.7578"/><line style="stroke:#A80036;stroke-width:1.0;" x1="315.5" x2="356.5" y1="416.7578" y2="416.7578"/><polygon fill="#A80036" points="325.5,412.7578,315.5,416.7578,325.5,420.7578,321.5,416.7578" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="136" x="321.5" y="398.6919">numbers comparison</text><path d="M243.5,431.7578 L422.5,431.7578 L422.5,438.7578 L412.5,448.7578 L243.5,448.7578 L243.5,431.7578 " fill="#EEEEEE" style="stroke:#000000;stroke-width:1.0;"/><rect fill="none" height="46.2656" style="stroke:#000000;stroke-width:2.0;" width="397.5" x="243.5" y="431.7578"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacing" textLength="134" x="258.5" y="444.8247">number not found</text><polygon fill="#A80036" points="567.5,466.0234,577.5,470.0234,567.5,474.0234,571.5,470.0234" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="573.5" y1="470.0234" y2="470.0234"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="77" x="321.5" y="464.9575">Add contact</text><path d="M243.5,492.0234 L436.5,492.0234 L436.5,499.0234 L426.5,509.0234 L243.5,509.0234 L243.5,492.0234 " fill="#EEEEEE" style="stroke:#000000;stroke-width:1.0;"/><rect fill="none" height="46.2656" style="stroke:#000000;stroke-width:2.0;" width="397.5" x="243.5" y="492.0234"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacing" textLength="148" x="258.5" y="505.0903">number found in db</text><polygon fill="#A80036" points="567.5,526.2891,577.5,530.2891,567.5,534.2891,571.5,530.2891" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="314.5" x2="573.5" y1="530.2891" y2="530.2891"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="241" x="321.5" y="525.2231">Update contact by overriding old data</text><polygon fill="#A80036" points="62.5,562.4219,52.5,566.4219,62.5,570.4219,58.5,566.4219" style="stroke:#A80036;stroke-width:1.0;"/><line style="stroke:#A80036;stroke-width:1.0;" x1="56.5" x2="313.5" y1="566.4219" y2="566.4219"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="103" x="68.5" y="561.356">status response</text><!--MD5=[921bcca0682cb10fe9b051cd54a58a3c]
@startuml

participant "Application" as app
participant "ContactRecord" as rec
participant "contacts DB" as db

== Checking for duplicates ==

app -> rec : contacts to check\n(query::CheckContactsListDuplicates)
rec -> db : get numbers
db -> rec
rec -> rec : numbers comparison
rec -> app : duplicated contacts

== Merging contacts list to DB ==

app -> rec : contacts to merge\n(query::MergeContactsList)
rec -> db : get numbers
db -> rec
rec -> rec : numbers comparison

group number not found
    rec -> db : Add contact
end
group number found in db
    rec -> db : Update contact by overriding old data
end
rec -> app : status response

@enduml

PlantUML version 1.2021.7(Sun May 23 14:40:07 CEST 2021)
(GPL source distribution)
Java Runtime: OpenJDK Runtime Environment
JVM: OpenJDK 64-Bit Server VM
Default Encoding: UTF-8
Language: pl
Country: PL
--></g></svg>
\ No newline at end of file