diff --git a/frontends/qt6/qmllib/dbal/DBALProvider.qml b/frontends/qt6/qmllib/dbal/DBALProvider.qml new file mode 100644 index 000000000..7aa554969 --- /dev/null +++ b/frontends/qt6/qmllib/dbal/DBALProvider.qml @@ -0,0 +1,217 @@ +import QtQuick +import QtQuick.Controls.Material + +/** + * QML DBAL Client Component + * + * Provides database operations for QML UI components. + * Wraps the C++ DBALClient for easy QML integration. + * + * Example: + * ```qml + * import "../qmllib/dbal" + * + * DBALProvider { + * id: dbal + * baseUrl: "http://localhost:3001/api/dbal" + * tenantId: "default" + * + * onConnectedChanged: { + * if (connected) { + * loadUsers() + * } + * } + * } + * + * function loadUsers() { + * dbal.list("User", { take: 20 }, function(result) { + * userModel.clear() + * for (var i = 0; i < result.items.length; i++) { + * userModel.append(result.items[i]) + * } + * }) + * } + * ``` + */ +Item { + id: root + + // Configuration + property string baseUrl: "http://localhost:3001/api/dbal" + property string tenantId: "default" + property string authToken: "" + + // State + property bool connected: false + property bool loading: false + property string lastError: "" + + // Signals + signal errorOccurred(string message) + signal operationCompleted(string operation, var result) + + // Internal HTTP client (simplified - would use XMLHttpRequest in real impl) + QtObject { + id: internal + + function request(method, endpoint, body, callback) { + root.loading = true + root.lastError = "" + + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + root.loading = false + + if (xhr.status >= 200 && xhr.status < 300) { + try { + var result = JSON.parse(xhr.responseText) + if (callback) callback(result, null) + root.operationCompleted(endpoint, result) + } catch (e) { + var err = "Failed to parse response: " + e.message + root.lastError = err + root.errorOccurred(err) + if (callback) callback(null, err) + } + } else { + var error = xhr.statusText || "Request failed" + root.lastError = error + root.errorOccurred(error) + if (callback) callback(null, error) + } + } + } + + var url = root.baseUrl + endpoint + xhr.open(method, url) + xhr.setRequestHeader("Content-Type", "application/json") + xhr.setRequestHeader("X-Tenant-ID", root.tenantId) + + if (root.authToken) { + xhr.setRequestHeader("Authorization", "Bearer " + root.authToken) + } + + if (body) { + xhr.send(JSON.stringify(body)) + } else { + xhr.send() + } + } + } + + // Public API + + /** + * Create a new record + * @param {string} entity - Entity name (e.g., "User", "AuditLog") + * @param {object} data - Record data + * @param {function} callback - Callback(result, error) + */ + function create(entity, data, callback) { + internal.request("POST", "/create", { + entity: entity, + data: data, + tenantId: tenantId + }, callback) + } + + /** + * Read a single record by ID + * @param {string} entity - Entity name + * @param {string} id - Record ID + * @param {function} callback - Callback(result, error) + */ + function read(entity, id, callback) { + internal.request("GET", "/read/" + entity + "/" + id, null, callback) + } + + /** + * Update an existing record + * @param {string} entity - Entity name + * @param {string} id - Record ID + * @param {object} data - Updated fields + * @param {function} callback - Callback(result, error) + */ + function update(entity, id, data, callback) { + internal.request("PUT", "/update", { + entity: entity, + id: id, + data: data + }, callback) + } + + /** + * Delete a record + * @param {string} entity - Entity name + * @param {string} id - Record ID + * @param {function} callback - Callback(success, error) + */ + function remove(entity, id, callback) { + internal.request("DELETE", "/delete/" + entity + "/" + id, null, callback) + } + + /** + * List records with pagination and filtering + * @param {string} entity - Entity name + * @param {object} options - { take, skip, where, orderBy } + * @param {function} callback - Callback({ items, total }, error) + */ + function list(entity, options, callback) { + var body = { + entity: entity, + tenantId: tenantId + } + + if (options.take !== undefined) body.take = options.take + if (options.skip !== undefined) body.skip = options.skip + if (options.where !== undefined) body.where = options.where + if (options.orderBy !== undefined) body.orderBy = options.orderBy + + internal.request("POST", "/list", body, callback) + } + + /** + * Find first record matching filter + * @param {string} entity - Entity name + * @param {object} filter - Filter criteria + * @param {function} callback - Callback(result, error) + */ + function findFirst(entity, filter, callback) { + internal.request("POST", "/findFirst", { + entity: entity, + tenantId: tenantId, + filter: filter + }, callback) + } + + /** + * Execute a named operation + * @param {string} operation - Operation name + * @param {object} params - Operation parameters + * @param {function} callback - Callback(result, error) + */ + function execute(operation, params, callback) { + internal.request("POST", "/execute", { + operation: operation, + params: params, + tenantId: tenantId + }, callback) + } + + /** + * Check connection to DBAL backend + * @param {function} callback - Callback(success, error) + */ + function ping(callback) { + internal.request("GET", "/ping", null, function(result, error) { + root.connected = !error + if (callback) callback(!error, error) + }) + } + + // Auto-ping on component ready + Component.onCompleted: { + ping() + } +} diff --git a/frontends/qt6/qmllib/dbal/qmldir b/frontends/qt6/qmllib/dbal/qmldir new file mode 100644 index 000000000..f60581471 --- /dev/null +++ b/frontends/qt6/qmllib/dbal/qmldir @@ -0,0 +1,3 @@ +module dbal + +DBALProvider 1.0 DBALProvider.qml diff --git a/frontends/qt6/src/DBALClient.cpp b/frontends/qt6/src/DBALClient.cpp new file mode 100644 index 000000000..dacd10aad --- /dev/null +++ b/frontends/qt6/src/DBALClient.cpp @@ -0,0 +1,199 @@ +#include "DBALClient.h" +#include +#include +#include +#include + +DBALClient::DBALClient(QObject *parent) + : QObject(parent) + , m_networkManager(new QNetworkAccessManager(this)) + , m_baseUrl("http://localhost:3001/api/dbal") + , m_tenantId("default") + , m_connected(false) +{ + connect(m_networkManager, &QNetworkAccessManager::finished, + this, &DBALClient::handleNetworkReply); +} + +DBALClient::~DBALClient() +{ + // Cleanup pending callbacks + m_pendingCallbacks.clear(); +} + +void DBALClient::setBaseUrl(const QString &url) +{ + if (m_baseUrl != url) { + m_baseUrl = url; + emit baseUrlChanged(); + } +} + +void DBALClient::setTenantId(const QString &id) +{ + if (m_tenantId != id) { + m_tenantId = id; + emit tenantIdChanged(); + } +} + +void DBALClient::setAuthToken(const QString &token) +{ + if (m_authToken != token) { + m_authToken = token; + emit authTokenChanged(); + } +} + +void DBALClient::setError(const QString &error) +{ + m_lastError = error; + emit errorOccurred(error); +} + +void DBALClient::sendRequest(const QString &method, const QString &endpoint, + const QJsonObject &body, const QJSValue &callback) +{ + QUrl url(m_baseUrl + endpoint); + QNetworkRequest request(url); + + // Set headers + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("X-Tenant-ID", m_tenantId.toUtf8()); + + if (!m_authToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_authToken).toUtf8()); + } + + QNetworkReply *reply = nullptr; + QByteArray jsonData = QJsonDocument(body).toJson(QJsonDocument::Compact); + + if (method == "GET") { + reply = m_networkManager->get(request); + } else if (method == "POST") { + reply = m_networkManager->post(request, jsonData); + } else if (method == "PUT") { + reply = m_networkManager->put(request, jsonData); + } else if (method == "DELETE") { + reply = m_networkManager->deleteResource(request); + } + + if (reply && callback.isCallable()) { + m_pendingCallbacks[reply] = callback; + } +} + +void DBALClient::handleNetworkReply(QNetworkReply *reply) +{ + reply->deleteLater(); + + QJSValue callback = m_pendingCallbacks.take(reply); + + if (reply->error() != QNetworkReply::NoError) { + setError(reply->errorString()); + + if (callback.isCallable()) { + QJSValueList args; + args << QJSValue::NullValue; + args << reply->errorString(); + callback.call(args); + } + return; + } + + QByteArray data = reply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data); + + if (callback.isCallable()) { + QJSValueList args; + // Convert QJsonObject to QJSValue through QVariant + if (doc.isObject()) { + args << callback.engine()->toScriptValue(doc.object().toVariantMap()); + } else if (doc.isArray()) { + args << callback.engine()->toScriptValue(doc.array().toVariantList()); + } else { + args << QJSValue::NullValue; + } + callback.call(args); + } + + // Update connected status + if (!m_connected) { + m_connected = true; + emit connectedChanged(); + } +} + +// CRUD Operations + +void DBALClient::create(const QString &entity, const QJsonObject &data, const QJSValue &callback) +{ + QJsonObject body; + body["entity"] = entity; + body["data"] = data; + body["tenantId"] = m_tenantId; + + sendRequest("POST", "/create", body, callback); +} + +void DBALClient::read(const QString &entity, const QString &id, const QJSValue &callback) +{ + QString endpoint = QString("/read/%1/%2").arg(entity, id); + sendRequest("GET", endpoint, QJsonObject(), callback); +} + +void DBALClient::update(const QString &entity, const QString &id, + const QJsonObject &data, const QJSValue &callback) +{ + QJsonObject body; + body["entity"] = entity; + body["id"] = id; + body["data"] = data; + + sendRequest("PUT", "/update", body, callback); +} + +void DBALClient::remove(const QString &entity, const QString &id, const QJSValue &callback) +{ + QString endpoint = QString("/delete/%1/%2").arg(entity, id); + sendRequest("DELETE", endpoint, QJsonObject(), callback); +} + +void DBALClient::list(const QString &entity, const QJsonObject &options, const QJSValue &callback) +{ + QJsonObject body; + body["entity"] = entity; + body["tenantId"] = m_tenantId; + + if (options.contains("take")) body["take"] = options["take"]; + if (options.contains("skip")) body["skip"] = options["skip"]; + if (options.contains("where")) body["where"] = options["where"]; + if (options.contains("orderBy")) body["orderBy"] = options["orderBy"]; + + sendRequest("POST", "/list", body, callback); +} + +void DBALClient::findFirst(const QString &entity, const QJsonObject &filter, const QJSValue &callback) +{ + QJsonObject body; + body["entity"] = entity; + body["tenantId"] = m_tenantId; + body["filter"] = filter; + + sendRequest("POST", "/findFirst", body, callback); +} + +void DBALClient::execute(const QString &operation, const QJsonObject ¶ms, const QJSValue &callback) +{ + QJsonObject body; + body["operation"] = operation; + body["params"] = params; + body["tenantId"] = m_tenantId; + + sendRequest("POST", "/execute", body, callback); +} + +void DBALClient::ping() +{ + sendRequest("GET", "/ping", QJsonObject(), QJSValue()); +}