Implement Milestone 2 profile schema, dialog, and connect lifecycle

This commit is contained in:
2026-03-01 09:21:53 -07:00
parent 87b0f60569
commit f8a81ebe36
9 changed files with 488 additions and 92 deletions
+183 -36
View File
@@ -1,6 +1,7 @@
#include "profile_repository.h"
#include <QDir>
#include <QSet>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
@@ -20,6 +21,35 @@ QString buildDatabasePath()
return dataDir.filePath(QStringLiteral("orbithub_profiles.sqlite"));
}
void bindProfileFields(QSqlQuery& query, const Profile& profile)
{
query.addBindValue(profile.name.trimmed());
query.addBindValue(profile.host.trimmed());
query.addBindValue(profile.port);
query.addBindValue(profile.username.trimmed());
query.addBindValue(profile.protocol.trimmed());
query.addBindValue(profile.authMode.trimmed());
}
Profile profileFromQuery(const QSqlQuery& query)
{
Profile profile;
profile.id = query.value(0).toLongLong();
profile.name = query.value(1).toString();
profile.host = query.value(2).toString();
profile.port = query.value(3).toInt();
profile.username = query.value(4).toString();
profile.protocol = query.value(5).toString();
profile.authMode = query.value(6).toString();
return profile;
}
bool isProfileValid(const Profile& profile)
{
return !profile.name.trimmed().isEmpty() && !profile.host.trimmed().isEmpty()
&& profile.port >= 1 && profile.port <= 65535;
}
}
ProfileRepository::ProfileRepository() : m_connectionName(QStringLiteral("orbithub_main"))
@@ -45,6 +75,11 @@ QString ProfileRepository::initError() const
return m_initError;
}
QString ProfileRepository::lastError() const
{
return m_lastError;
}
std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery) const
{
std::vector<Profile> result;
@@ -53,68 +88,115 @@ std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery)
return result;
}
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
if (searchQuery.trimmed().isEmpty()) {
query.prepare(QStringLiteral(
"SELECT id, name FROM profiles ORDER BY lower(name) ASC, id ASC"));
"SELECT id, name, host, port, username, protocol, auth_mode "
"FROM profiles "
"ORDER BY lower(name) ASC, id ASC"));
} else {
query.prepare(QStringLiteral(
"SELECT id, name FROM profiles "
"WHERE lower(name) LIKE lower(?) "
"SELECT id, name, host, port, username, protocol, auth_mode "
"FROM profiles "
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) "
"ORDER BY lower(name) ASC, id ASC"));
query.addBindValue(QStringLiteral("%") + searchQuery.trimmed() + QStringLiteral("%"));
const QString search = QStringLiteral("%") + searchQuery.trimmed() + QStringLiteral("%");
query.addBindValue(search);
query.addBindValue(search);
}
if (!query.exec()) {
setLastError(query.lastError().text());
return result;
}
while (query.next()) {
result.push_back(Profile{query.value(0).toLongLong(), query.value(1).toString()});
result.push_back(profileFromQuery(query));
}
return result;
}
std::optional<Profile> ProfileRepository::createProfile(const QString& name) const
std::optional<Profile> ProfileRepository::getProfile(qint64 id) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return std::nullopt;
}
const QString trimmedName = name.trimmed();
if (trimmedName.isEmpty()) {
return std::nullopt;
}
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral("INSERT INTO profiles(name) VALUES (?)"));
query.addBindValue(trimmedName);
if (!query.exec()) {
return std::nullopt;
}
return Profile{query.lastInsertId().toLongLong(), trimmedName};
}
bool ProfileRepository::updateProfile(qint64 id, const QString& name) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return false;
}
const QString trimmedName = name.trimmed();
if (trimmedName.isEmpty()) {
return false;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral("UPDATE profiles SET name = ? WHERE id = ?"));
query.addBindValue(trimmedName);
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode "
"FROM profiles WHERE id = ?"));
query.addBindValue(id);
if (!query.exec()) {
setLastError(query.lastError().text());
return std::nullopt;
}
if (!query.next()) {
return std::nullopt;
}
return profileFromQuery(query);
}
std::optional<Profile> ProfileRepository::createProfile(const Profile& profile) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return std::nullopt;
}
setLastError(QString());
if (!isProfileValid(profile)) {
setLastError(QStringLiteral("Name, host, and a valid port are required."));
return std::nullopt;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"INSERT INTO profiles(name, host, port, username, protocol, auth_mode) "
"VALUES (?, ?, ?, ?, ?, ?)"));
bindProfileFields(query, profile);
if (!query.exec()) {
setLastError(query.lastError().text());
return std::nullopt;
}
Profile created = profile;
created.id = query.lastInsertId().toLongLong();
return created;
}
bool ProfileRepository::updateProfile(const Profile& profile) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return false;
}
setLastError(QString());
if (profile.id < 0 || !isProfileValid(profile)) {
setLastError(QStringLiteral("Invalid profile data."));
return false;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"UPDATE profiles "
"SET name = ?, host = ?, port = ?, username = ?, protocol = ?, auth_mode = ? "
"WHERE id = ?"));
bindProfileFields(query, profile);
query.addBindValue(profile.id);
if (!query.exec()) {
setLastError(query.lastError().text());
return false;
}
@@ -127,11 +209,14 @@ bool ProfileRepository::deleteProfile(qint64 id) const
return false;
}
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral("DELETE FROM profiles WHERE id = ?"));
query.addBindValue(id);
if (!query.exec()) {
setLastError(query.lastError().text());
return false;
}
@@ -152,12 +237,74 @@ bool ProfileRepository::initializeDatabase()
const bool created = query.exec(QStringLiteral(
"CREATE TABLE IF NOT EXISTS profiles ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL UNIQUE"
"name TEXT NOT NULL UNIQUE,"
"host TEXT NOT NULL DEFAULT '',"
"port INTEGER NOT NULL DEFAULT 22,"
"username TEXT NOT NULL DEFAULT '',"
"protocol TEXT NOT NULL DEFAULT 'SSH',"
"auth_mode TEXT NOT NULL DEFAULT 'Password'"
")"));
if (!created) {
m_initError = query.lastError().text();
return false;
}
return created;
if (!ensureProfileSchema()) {
m_initError = m_lastError;
return false;
}
return true;
}
bool ProfileRepository::ensureProfileSchema() const
{
if (!QSqlDatabase::contains(m_connectionName)) {
setLastError(QStringLiteral("Database connection missing."));
return false;
}
QSqlQuery tableInfo(QSqlDatabase::database(m_connectionName));
if (!tableInfo.exec(QStringLiteral("PRAGMA table_info(profiles)"))) {
setLastError(tableInfo.lastError().text());
return false;
}
QSet<QString> columns;
while (tableInfo.next()) {
columns.insert(tableInfo.value(1).toString());
}
struct ColumnDef {
QString name;
QString ddl;
};
const std::vector<ColumnDef> required = {
{QStringLiteral("host"), QStringLiteral("ALTER TABLE profiles ADD COLUMN host TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("port"), QStringLiteral("ALTER TABLE profiles ADD COLUMN port INTEGER NOT NULL DEFAULT 22")},
{QStringLiteral("username"), QStringLiteral("ALTER TABLE profiles ADD COLUMN username TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("protocol"), QStringLiteral("ALTER TABLE profiles ADD COLUMN protocol TEXT NOT NULL DEFAULT 'SSH'")},
{QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")}};
for (const ColumnDef& column : required) {
if (columns.contains(column.name)) {
continue;
}
QSqlQuery alter(QSqlDatabase::database(m_connectionName));
if (!alter.exec(column.ddl)) {
setLastError(alter.lastError().text());
return false;
}
}
setLastError(QString());
return true;
}
void ProfileRepository::setLastError(const QString& error) const
{
m_lastError = error;
}