diff --git a/common/storage/entitystore.cpp b/common/storage/entitystore.cpp index 52c5caaf..e8ec5ab3 100644 --- a/common/storage/entitystore.cpp +++ b/common/storage/entitystore.cpp @@ -1,704 +1,704 @@ /* * Copyright (C) 2016 Christian Mollekopf * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "entitystore.h" #include #include #include "entitybuffer.h" #include "log.h" #include "typeindex.h" #include "definitions.h" #include "resourcecontext.h" #include "index.h" #include "bufferutils.h" #include "entity_generated.h" #include "applicationdomaintype_p.h" #include "typeimplementations.h" using namespace Sink; using namespace Sink::Storage; static QMap baseDbs() { return {{"revisionType", Storage::IntegerKeys}, {"revisions", Storage::IntegerKeys}, {"uidsToRevisions", Storage::AllowDuplicates | Storage::IntegerValues}, {"uids", 0}, {"default", 0}, {"__flagtable", 0}}; } template void mergeImpl(T &map, First f) { for (auto it = f.constBegin(); it != f.constEnd(); it++) { map.insert(it.key(), it.value()); } } template void mergeImpl(T &map, First f, Tail ...maps) { for (auto it = f.constBegin(); it != f.constEnd(); it++) { map.insert(it.key(), it.value()); } mergeImpl(map, maps...); } template First merge(First f, Tail ...maps) { First map; mergeImpl(map, f, maps...); return map; } template struct DbLayoutHelper { void operator()(QMap map) const { mergeImpl(map, ApplicationDomain::TypeImplementation::typeDatabases()); } }; static Sink::Storage::DbLayout dbLayout(const QByteArray &instanceId) { static auto databases = [] { QMap map; mergeImpl(map, ApplicationDomain::TypeImplementation::typeDatabases()); mergeImpl(map, ApplicationDomain::TypeImplementation::typeDatabases()); mergeImpl(map, ApplicationDomain::TypeImplementation::typeDatabases()); mergeImpl(map, ApplicationDomain::TypeImplementation::typeDatabases()); mergeImpl(map, ApplicationDomain::TypeImplementation::typeDatabases()); mergeImpl(map, ApplicationDomain::TypeImplementation::typeDatabases()); mergeImpl(map, ApplicationDomain::TypeImplementation::typeDatabases()); return merge(baseDbs(), map); }(); return {instanceId, databases}; } class EntityStore::Private { public: Private(const ResourceContext &context, const Sink::Log::Context &ctx) : resourceContext(context), logCtx(ctx.subContext("entitystore")) { } ResourceContext resourceContext; DataStore::Transaction transaction; QHash > indexByType; Sink::Log::Context logCtx; bool exists() { return Storage::DataStore::exists(Sink::storageLocation(), resourceContext.instanceId()); } DataStore::Transaction &getTransaction() { if (transaction) { return transaction; } DataStore store(Sink::storageLocation(), dbLayout(resourceContext.instanceId()), DataStore::ReadOnly); transaction = store.createTransaction(DataStore::ReadOnly); return transaction; } template struct ConfigureHelper { void operator()(TypeIndex &arg) const { ApplicationDomain::TypeImplementation::configure(arg); } }; TypeIndex &cachedIndex(const QByteArray &type) { if (indexByType.contains(type)) { return *indexByType.value(type); } auto index = QSharedPointer::create(type, logCtx); TypeHelper{type}.template operator()(*index); indexByType.insert(type, index); return *index; } TypeIndex &typeIndex(const QByteArray &type) { auto &index = cachedIndex(type); index.mTransaction = &transaction; return index; } ApplicationDomainType createApplicationDomainType(const QByteArray &type, const QByteArray &uid, qint64 revision, const EntityBuffer &buffer) { auto adaptor = resourceContext.adaptorFactory(type).createAdaptor(buffer.entity(), &typeIndex(type)); return ApplicationDomainType{resourceContext.instanceId(), uid, revision, adaptor}; } }; EntityStore::EntityStore(const ResourceContext &context, const Log::Context &ctx) : d(new EntityStore::Private{context, ctx}) { } void EntityStore::initialize() { //This function is only called in the resource code where we want to be able to write to the databse. //Check for the existience of the db without creating it or the envrionment. //This is required to be able to set the database version only in the case where we create a new database. if (!Storage::DataStore::exists(Sink::storageLocation(), d->resourceContext.instanceId())) { //The first time we open the environment we always want it to be read/write. Otherwise subsequent tries to open a write transaction will fail. startTransaction(DataStore::ReadWrite); //Create the database with the correct version if it wasn't existing before SinkLogCtx(d->logCtx) << "Creating resource database."; Storage::DataStore::setDatabaseVersion(d->transaction, Sink::latestDatabaseVersion()); } else { //The first time we open the environment we always want it to be read/write. Otherwise subsequent tries to open a write transaction will fail. startTransaction(DataStore::ReadWrite); } commitTransaction(); } void EntityStore::startTransaction(DataStore::AccessMode accessMode) { SinkTraceCtx(d->logCtx) << "Starting transaction: " << accessMode; Q_ASSERT(!d->transaction); d->transaction = DataStore(Sink::storageLocation(), dbLayout(d->resourceContext.instanceId()), accessMode).createTransaction(accessMode); } void EntityStore::commitTransaction() { SinkTraceCtx(d->logCtx) << "Committing transaction"; for (const auto &type : d->indexByType.keys()) { d->typeIndex(type).commitTransaction(); } Q_ASSERT(d->transaction); d->transaction.commit(); d->transaction = {}; } void EntityStore::abortTransaction() { SinkTraceCtx(d->logCtx) << "Aborting transaction"; d->transaction.abort(); d->transaction = {}; } bool EntityStore::hasTransaction() const { return d->transaction; } bool EntityStore::add(const QByteArray &type, ApplicationDomainType entity, bool replayToSource) { if (entity.identifier().isEmpty()) { SinkWarningCtx(d->logCtx) << "Can't write entity with an empty identifier"; return false; } SinkTraceCtx(d->logCtx) << "New entity " << entity; const auto identifier = Identifier::fromDisplayByteArray(entity.identifier()); d->typeIndex(type).add(identifier, entity, d->transaction, d->resourceContext.instanceId()); //The maxRevision may have changed meanwhile if the entity created sub-entities const qint64 newRevision = maxRevision() + 1; // Add metadata buffer flatbuffers::FlatBufferBuilder metadataFbb; auto metadataBuilder = MetadataBuilder(metadataFbb); metadataBuilder.add_revision(newRevision); metadataBuilder.add_operation(Operation_Creation); metadataBuilder.add_replayToSource(replayToSource); auto metadataBuffer = metadataBuilder.Finish(); FinishMetadataBuffer(metadataFbb, metadataBuffer); flatbuffers::FlatBufferBuilder fbb; d->resourceContext.adaptorFactory(type).createBuffer(entity, fbb, metadataFbb.GetBufferPointer(), metadataFbb.GetSize()); const auto key = Key(identifier, newRevision); DataStore::mainDatabase(d->transaction, type) .write(newRevision, BufferUtils::extractBuffer(fbb), [&](const DataStore::Error &error) { SinkWarningCtx(d->logCtx) << "Failed to write entity" << entity.identifier() << newRevision; }); DataStore::setMaxRevision(d->transaction, newRevision); DataStore::recordRevision(d->transaction, newRevision, entity.identifier(), type); DataStore::recordUid(d->transaction, entity.identifier(), type); SinkTraceCtx(d->logCtx) << "Wrote entity: " << key << "of type:" << type; return true; } ApplicationDomain::ApplicationDomainType EntityStore::applyDiff(const QByteArray &type, const ApplicationDomainType ¤t, const ApplicationDomainType &diff, const QByteArrayList &deletions, const QSet &excludeProperties) const { SinkTraceCtx(d->logCtx) << "Applying diff: " << current.availableProperties() << "Deletions: " << deletions << "Changeset: " << diff.changedProperties() << "Excluded: " << excludeProperties; auto newEntity = *ApplicationDomainType::getInMemoryRepresentation(current, current.availableProperties()); // Apply diff for (const auto &property : diff.changedProperties()) { if (!excludeProperties.contains(property)) { const auto value = diff.getProperty(property); if (value.isValid()) { newEntity.setProperty(property, value); } } } // Remove deletions for (const auto &property : deletions) { if (!excludeProperties.contains(property)) { newEntity.setProperty(property, QVariant()); } } return newEntity; } bool EntityStore::modify(const QByteArray &type, const ApplicationDomainType &diff, const QByteArrayList &deletions, bool replayToSource) { const auto current = readLatest(type, diff.identifier()); if (current.identifier().isEmpty()) { SinkWarningCtx(d->logCtx) << "Failed to read current version: " << diff.identifier(); return false; } auto newEntity = applyDiff(type, current, diff, deletions); return modify(type, current, newEntity, replayToSource); } bool EntityStore::modify(const QByteArray &type, const ApplicationDomainType ¤t, ApplicationDomainType newEntity, bool replayToSource) { SinkTraceCtx(d->logCtx) << "Modified entity: " << newEntity; const auto identifier = Identifier::fromDisplayByteArray(newEntity.identifier()); d->typeIndex(type).modify(identifier, current, newEntity, d->transaction, d->resourceContext.instanceId()); const qint64 newRevision = DataStore::maxRevision(d->transaction) + 1; // Add metadata buffer flatbuffers::FlatBufferBuilder metadataFbb; { //We add availableProperties to account for the properties that have been changed by the preprocessors auto modifiedProperties = BufferUtils::toVector(metadataFbb, newEntity.changedProperties()); auto metadataBuilder = MetadataBuilder(metadataFbb); metadataBuilder.add_revision(newRevision); metadataBuilder.add_operation(Operation_Modification); metadataBuilder.add_replayToSource(replayToSource); metadataBuilder.add_modifiedProperties(modifiedProperties); auto metadataBuffer = metadataBuilder.Finish(); FinishMetadataBuffer(metadataFbb, metadataBuffer); } SinkTraceCtx(d->logCtx) << "Changed properties: " << newEntity.changedProperties(); newEntity.setChangedProperties(newEntity.availableProperties().toSet()); flatbuffers::FlatBufferBuilder fbb; d->resourceContext.adaptorFactory(type).createBuffer(newEntity, fbb, metadataFbb.GetBufferPointer(), metadataFbb.GetSize()); DataStore::mainDatabase(d->transaction, type) .write(newRevision, BufferUtils::extractBuffer(fbb), [&](const DataStore::Error &error) { SinkWarningCtx(d->logCtx) << "Failed to write entity" << newEntity.identifier() << newRevision; }); DataStore::setMaxRevision(d->transaction, newRevision); DataStore::recordRevision(d->transaction, newRevision, newEntity.identifier(), type); SinkTraceCtx(d->logCtx) << "Wrote modified entity: " << newEntity.identifier() << type << newRevision; return true; } bool EntityStore::remove(const QByteArray &type, const ApplicationDomainType ¤t, bool replayToSource) { const auto uid = current.identifier(); if (!exists(type, uid)) { SinkWarningCtx(d->logCtx) << "Remove: Entity is already removed " << uid; return false; } const auto identifier = Identifier::fromDisplayByteArray(uid); d->typeIndex(type).remove(identifier, current, d->transaction, d->resourceContext.instanceId()); SinkTraceCtx(d->logCtx) << "Removed entity " << current; const qint64 newRevision = DataStore::maxRevision(d->transaction) + 1; // Add metadata buffer flatbuffers::FlatBufferBuilder metadataFbb; auto metadataBuilder = MetadataBuilder(metadataFbb); metadataBuilder.add_revision(newRevision); metadataBuilder.add_operation(Operation_Removal); metadataBuilder.add_replayToSource(replayToSource); auto metadataBuffer = metadataBuilder.Finish(); FinishMetadataBuffer(metadataFbb, metadataBuffer); flatbuffers::FlatBufferBuilder fbb; EntityBuffer::assembleEntityBuffer(fbb, metadataFbb.GetBufferPointer(), metadataFbb.GetSize(), 0, 0, 0, 0); DataStore::mainDatabase(d->transaction, type) .write(newRevision, BufferUtils::extractBuffer(fbb), [&](const DataStore::Error &error) { SinkWarningCtx(d->logCtx) << "Failed to write entity" << uid << newRevision; }); DataStore::setMaxRevision(d->transaction, newRevision); DataStore::recordRevision(d->transaction, newRevision, uid, type); DataStore::removeUid(d->transaction, uid, type); return true; } void EntityStore::cleanupEntityRevisionsUntil(qint64 revision) { const auto uid = DataStore::getUidFromRevision(d->transaction, revision); const auto bufferType = DataStore::getTypeFromRevision(d->transaction, revision); if (bufferType.isEmpty() || uid.isEmpty()) { SinkErrorCtx(d->logCtx) << "Failed to find revision during cleanup: " << revision; Q_ASSERT(false); return; } SinkTraceCtx(d->logCtx) << "Cleaning up revision " << revision << uid << bufferType; const auto internalUid = Identifier::fromDisplayByteArray(uid).toInternalByteArray(); // Remove old revisions const auto revisionsToRemove = DataStore::getRevisionsUntilFromUid(d->transaction, uid, revision); for (const auto &revisionToRemove : revisionsToRemove) { DataStore::removeRevision(d->transaction, revisionToRemove); DataStore::mainDatabase(d->transaction, bufferType).remove(revisionToRemove); } // And remove the specified revision only if marked for removal DataStore::mainDatabase(d->transaction, bufferType).scan(revision, [&](size_t, const QByteArray &data) { EntityBuffer buffer(const_cast(data.data()), data.size()); if (!buffer.isValid()) { SinkWarningCtx(d->logCtx) << "Read invalid buffer from disk"; return false; } const auto metadata = flatbuffers::GetRoot(buffer.metadataBuffer()); if (metadata->operation() == Operation_Removal) { DataStore::removeRevision(d->transaction, revision); DataStore::mainDatabase(d->transaction, bufferType).remove(revision); } return false; }); DataStore::setCleanedUpRevision(d->transaction, revision); } bool EntityStore::cleanupRevisions(qint64 revision) { Q_ASSERT(d->exists()); bool implicitTransaction = false; if (!d->transaction) { startTransaction(DataStore::ReadWrite); Q_ASSERT(d->transaction); implicitTransaction = true; } const auto lastCleanRevision = DataStore::cleanedUpRevision(d->transaction); const auto firstRevisionToCleanup = lastCleanRevision + 1; bool cleanupIsNecessary = firstRevisionToCleanup <= revision; if (cleanupIsNecessary) { SinkTraceCtx(d->logCtx) << "Cleaning up from " << firstRevisionToCleanup << " to " << revision; for (qint64 rev = firstRevisionToCleanup; rev <= revision; rev++) { cleanupEntityRevisionsUntil(rev); } } if (implicitTransaction) { commitTransaction(); } return cleanupIsNecessary; } QVector EntityStore::fullScan(const QByteArray &type) { SinkTraceCtx(d->logCtx) << "Looking for : " << type; if (!d->exists()) { SinkTraceCtx(d->logCtx) << "Database is not existing: " << type; return {}; } QSet keys; DataStore::getUids(type, d->getTransaction(), [&keys] (const QByteArray &uid) { keys << Identifier::fromDisplayByteArray(uid); }); SinkTraceCtx(d->logCtx) << "Full scan retrieved " << keys.size() << " results."; return keys.toList().toVector(); } QVector EntityStore::indexLookup(const QByteArray &type, const QueryBase &query, QSet &appliedFilters, QByteArray &appliedSorting) { if (!d->exists()) { SinkTraceCtx(d->logCtx) << "Database is not existing: " << type; return {}; } return d->typeIndex(type).query(query, appliedFilters, appliedSorting, d->getTransaction(), d->resourceContext.instanceId()); } QVector EntityStore::indexLookup(const QByteArray &type, const QByteArray &property, const QVariant &value) { if (!d->exists()) { SinkTraceCtx(d->logCtx) << "Database is not existing: " << type; return {}; } return d->typeIndex(type).lookup(property, value, d->getTransaction()); } void EntityStore::indexLookup(const QByteArray &type, const QByteArray &property, const QVariant &value, const std::function &callback) { if (!d->exists()) { SinkTraceCtx(d->logCtx) << "Database is not existing: " << type; return; } auto list = indexLookup(type, property, value); for (const auto &id : list) { callback(id.toDisplayByteArray()); } /* Index index(type + ".index." + property, d->transaction); */ /* index.lookup(value, [&](const QByteArray &sinkId) { */ /* callback(sinkId); */ /* }, */ /* [&](const Index::Error &error) { */ /* SinkWarningCtx(d->logCtx) << "Error in index: " << error.message << property; */ /* }); */ } void EntityStore::readLatest(const QByteArray &type, const Identifier &id, const std::function callback) { Q_ASSERT(d); const size_t revision = DataStore::getLatestRevisionFromUid(d->getTransaction(), id.toDisplayByteArray()); auto db = DataStore::mainDatabase(d->getTransaction(), type); db.scan(revision, [=](size_t, const QByteArray &value) { callback(id.toDisplayByteArray(), Sink::EntityBuffer(value.data(), value.size())); return false; }, [&](const DataStore::Error &error) { SinkWarningCtx(d->logCtx) << "Error during readLatest query: " << error.message << id; }); } void EntityStore::readLatest(const QByteArray &type, const QByteArray &uid, const std::function callback) { readLatest(type, Identifier::fromDisplayByteArray(uid), callback); } void EntityStore::readLatest(const QByteArray &type, const Identifier &uid, const std::function callback) { readLatest(type, uid, [&](const QByteArray &uid, const EntityBuffer &buffer) { //TODO cache max revision for the duration of the transaction. callback(d->createApplicationDomainType(type, uid, DataStore::maxRevision(d->getTransaction()), buffer)); }); } void EntityStore::readLatest(const QByteArray &type, const QByteArray &uid, const std::function callback) { readLatest(type, Identifier::fromDisplayByteArray(uid), callback); } void EntityStore::readLatest(const QByteArray &type, const Identifier &uid, const std::function callback) { readLatest(type, uid, [&](const QByteArray &uid, const EntityBuffer &buffer) { //TODO cache max revision for the duration of the transaction. callback(d->createApplicationDomainType(type, uid, DataStore::maxRevision(d->getTransaction()), buffer), buffer.operation()); }); } void EntityStore::readLatest(const QByteArray &type, const QByteArray &uid, const std::function callback) { readLatest(type, Identifier::fromDisplayByteArray(uid), callback); } ApplicationDomain::ApplicationDomainType EntityStore::readLatest(const QByteArray &type, const QByteArray &uid) { ApplicationDomainType dt; readLatest(type, uid, [&](const ApplicationDomainType &entity) { dt = *ApplicationDomainType::getInMemoryRepresentation(entity, entity.availableProperties()); }); return dt; } void EntityStore::readEntity(const QByteArray &type, const QByteArray &displayKey, const std::function callback) { const auto key = Key::fromDisplayByteArray(displayKey); auto db = DataStore::mainDatabase(d->getTransaction(), type); db.scan(key.revision().toSizeT(), [=](size_t rev, const QByteArray &value) -> bool { const auto uid = DataStore::getUidFromRevision(d->transaction, rev); callback(uid, Sink::EntityBuffer(value.data(), value.size())); return false; }, [&](const DataStore::Error &error) { SinkWarningCtx(d->logCtx) << "Error during readEntity query: " << error.message << key; }); } void EntityStore::readEntity(const QByteArray &type, const QByteArray &uid, const std::function callback) { readEntity(type, uid, [&](const QByteArray &uid, const EntityBuffer &buffer) { callback(d->createApplicationDomainType(type, uid, DataStore::maxRevision(d->getTransaction()), buffer)); }); } ApplicationDomain::ApplicationDomainType EntityStore::readEntity(const QByteArray &type, const QByteArray &uid) { ApplicationDomainType dt; readEntity(type, uid, [&](const ApplicationDomainType &entity) { dt = *ApplicationDomainType::getInMemoryRepresentation(entity, entity.availableProperties()); }); return dt; } void EntityStore::readAll(const QByteArray &type, const std::function &callback) { readAllUids(type, [&] (const QByteArray &uid) { readLatest(type, uid, callback); }); } void EntityStore::readRevisions(qint64 baseRevision, const QByteArray &expectedType, const std::function &callback) { qint64 revisionCounter = baseRevision; const qint64 topRevision = DataStore::maxRevision(d->getTransaction()); // Spit out the revision keys one by one. while (revisionCounter <= topRevision) { const auto uid = DataStore::getUidFromRevision(d->getTransaction(), revisionCounter); const auto type = DataStore::getTypeFromRevision(d->getTransaction(), revisionCounter); // SinkTrace() << "Revision" << *revisionCounter << type << uid; Q_ASSERT(!uid.isEmpty()); Q_ASSERT(!type.isEmpty()); if (type != expectedType) { // Skip revision revisionCounter++; continue; } const auto key = Key(Identifier::fromDisplayByteArray(uid), revisionCounter); revisionCounter++; callback(key); } } void EntityStore::readPrevious(const QByteArray &type, const Identifier &id, qint64 revision, const std::function callback) { const auto previousRevisions = DataStore::getRevisionsUntilFromUid(d->getTransaction(), id.toDisplayByteArray(), revision); const size_t latestRevision = previousRevisions[previousRevisions.size() - 1]; const auto key = Key(id, latestRevision); readEntity(type, key.toDisplayByteArray(), callback); } void EntityStore::readPrevious(const QByteArray &type, const Identifier &id, qint64 revision, const std::function callback) { readPrevious(type, id, revision, [&](const QByteArray &uid, const EntityBuffer &buffer) { callback(d->createApplicationDomainType(type, uid, DataStore::maxRevision(d->getTransaction()), buffer)); }); } ApplicationDomain::ApplicationDomainType EntityStore::readPrevious(const QByteArray &type, const Identifier &id, qint64 revision) { ApplicationDomainType dt; readPrevious(type, id, revision, [&](const ApplicationDomainType &entity) { dt = *ApplicationDomainType::getInMemoryRepresentation(entity, entity.availableProperties()); }); return dt; } void EntityStore::readAllUids(const QByteArray &type, const std::function callback) { DataStore::getUids(type, d->getTransaction(), callback); } bool EntityStore::contains(const QByteArray & /* type */, const QByteArray &uid) { Q_ASSERT(!uid.isEmpty()); return !DataStore::getRevisionsFromUid(d->getTransaction(), uid).isEmpty(); } bool EntityStore::exists(const QByteArray &type, const QByteArray &uid) { bool found = false; bool alreadyRemoved = false; const size_t revision = DataStore::getLatestRevisionFromUid(d->getTransaction(), uid); DataStore::mainDatabase(d->transaction, type) .scan(revision, [&found, &alreadyRemoved](size_t, const QByteArray &data) { auto entity = GetEntity(data.data()); if (entity && entity->metadata()) { auto metadata = GetMetadata(entity->metadata()->Data()); found = true; if (metadata->operation() == Operation_Removal) { alreadyRemoved = true; } } return true; }, [&](const DataStore::Error &error) { SinkWarningCtx(d->logCtx) << "Failed to read old revision from storage: " << error.message; }); if (!found) { SinkTraceCtx(d->logCtx) << "Remove: Failed to find entity " << uid; return false; } if (alreadyRemoved) { SinkTraceCtx(d->logCtx) << "Remove: Entity is already removed " << uid; return false; } return true; } -void EntityStore::readRevisions(const QByteArray &type, const QByteArray &uid, qint64 startingRevision, +void EntityStore::readRevisions(const QByteArray &type, const QByteArray &uid, size_t startingRevision, const std::function callback) { Q_ASSERT(d); Q_ASSERT(!uid.isEmpty()); const auto revisions = DataStore::getRevisionsFromUid(d->transaction, uid); const auto db = DataStore::mainDatabase(d->transaction, type); for (const auto revision : revisions) { if (revision < static_cast(startingRevision)) { continue; } db.scan(revision, [&](size_t rev, const QByteArray &value) { Q_ASSERT(rev == revision); callback(uid, revision, Sink::EntityBuffer(value.data(), value.size())); return false; }, [&](const DataStore::Error &error) { SinkWarningCtx(d->logCtx) << "Error while reading: " << error.message; }, true); } } qint64 EntityStore::maxRevision() { if (!d->exists()) { SinkTraceCtx(d->logCtx) << "Database is not existing."; return 0; } return DataStore::maxRevision(d->getTransaction()); } Sink::Log::Context EntityStore::logContext() const { return d->logCtx; } diff --git a/common/storage/entitystore.h b/common/storage/entitystore.h index 79797987..e63cc8a9 100644 --- a/common/storage/entitystore.h +++ b/common/storage/entitystore.h @@ -1,146 +1,146 @@ /* * Copyright (C) 2016 Christian Mollekopf * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #pragma once #include "sink_export.h" #include #include "domaintypeadaptorfactoryinterface.h" #include "query.h" #include "storage.h" #include "key.h" #include "resourcecontext.h" #include "metadata_generated.h" namespace Sink { class EntityBuffer; namespace Storage { class SINK_EXPORT EntityStore { public: typedef QSharedPointer Ptr; EntityStore(const ResourceContext &resourceContext, const Sink::Log::Context &); ~EntityStore() = default; using ApplicationDomainType = ApplicationDomain::ApplicationDomainType; void initialize(); //Only the pipeline may call the following functions outside of tests bool add(const QByteArray &type, ApplicationDomainType newEntity, bool replayToSource); bool modify(const QByteArray &type, const ApplicationDomainType &diff, const QByteArrayList &deletions, bool replayToSource); bool modify(const QByteArray &type, const ApplicationDomainType ¤t, ApplicationDomainType newEntity, bool replayToSource); bool remove(const QByteArray &type, const ApplicationDomainType ¤t, bool replayToSource); bool cleanupRevisions(qint64 revision); ApplicationDomainType applyDiff(const QByteArray &type, const ApplicationDomainType ¤t, const ApplicationDomainType &diff, const QByteArrayList &deletions, const QSet &excludeProperties = {}) const; void startTransaction(Sink::Storage::DataStore::AccessMode); void commitTransaction(); void abortTransaction(); bool hasTransaction() const; QVector fullScan(const QByteArray &type); QVector indexLookup(const QByteArray &type, const QueryBase &query, QSet &appliedFilters, QByteArray &appliedSorting); QVector indexLookup(const QByteArray &type, const QByteArray &property, const QVariant &value); void indexLookup(const QByteArray &type, const QByteArray &property, const QVariant &value, const std::function &callback); template void indexLookup(const QVariant &value, const std::function &callback) { return indexLookup(ApplicationDomain::getTypeName(), PropertyType::name, value, callback); } ///Returns the uid and buffer. Note that the memory only remains valid until the next operation or transaction end. void readLatest(const QByteArray &type, const Identifier &uid, const std::function callback); void readLatest(const QByteArray &type, const QByteArray &uid, const std::function callback); ///Returns an entity. Note that the memory only remains valid until the next operation or transaction end. void readLatest(const QByteArray &type, const Identifier &uid, const std::function callback); void readLatest(const QByteArray &type, const QByteArray &uid, const std::function callback); ///Returns an entity and operation. Note that the memory only remains valid until the next operation or transaction end. void readLatest(const QByteArray &type, const Identifier &uid, const std::function callback); void readLatest(const QByteArray &type, const QByteArray &uid, const std::function callback); ///Returns a copy ApplicationDomainType readLatest(const QByteArray &type, const QByteArray &uid); template T readLatest(const QByteArray &uid) { return T(readLatest(ApplicationDomain::getTypeName(), uid)); } ///Returns the uid and buffer. Note that the memory only remains valid until the next operation or transaction end. void readEntity(const QByteArray &type, const QByteArray &uid, const std::function callback); ///Returns an entity. Note that the memory only remains valid until the next operation or transaction end. void readEntity(const QByteArray &type, const QByteArray &uid, const std::function callback); ///Returns a copy ApplicationDomainType readEntity(const QByteArray &type, const QByteArray &key); template T readEntity(const QByteArray &key) { return T(readEntity(ApplicationDomain::getTypeName(), key)); } void readPrevious(const QByteArray &type, const Sink::Storage::Identifier &id, qint64 revision, const std::function callback); void readPrevious(const QByteArray &type, const Sink::Storage::Identifier &id, qint64 revision, const std::function callback); ///Returns a copy ApplicationDomainType readPrevious(const QByteArray &type, const Sink::Storage::Identifier &id, qint64 revision); template T readPrevious(const QByteArray &uid, qint64 revision) { return T(readPrevious(ApplicationDomain::getTypeName(), uid, revision)); } void readAllUids(const QByteArray &type, const std::function callback); void readAll(const QByteArray &type, const std::function &callback); template void readAll(const std::function &callback) { return readAll(ApplicationDomain::getTypeName(), [&](const ApplicationDomainType &entity) { callback(T(entity)); }); } void readRevisions(qint64 baseRevision, const QByteArray &type, const std::function &callback); ///Db contains entity (but may already be marked as removed bool contains(const QByteArray &type, const QByteArray &uid); ///Db contains entity and entity is not yet removed bool exists(const QByteArray &type, const QByteArray &uid); - void readRevisions(const QByteArray &type, const QByteArray &uid, qint64 baseRevision, const std::function callback); + void readRevisions(const QByteArray &type, const QByteArray &uid, size_t baseRevision, const std::function callback); qint64 maxRevision(); Sink::Log::Context logContext() const; private: /* * Remove any old revisions of the same entity up until @param revision */ void cleanupEntityRevisionsUntil(qint64 revision); void copyBlobs(ApplicationDomainType &entity, qint64 newRevision); class Private; const QSharedPointer d; }; } } diff --git a/examples/caldavresource/caldavresource.cpp b/examples/caldavresource/caldavresource.cpp index 4a735212..bef52994 100644 --- a/examples/caldavresource/caldavresource.cpp +++ b/examples/caldavresource/caldavresource.cpp @@ -1,254 +1,255 @@ /* * Copyright (C) 2018 Christian Mollekopf * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "caldavresource.h" #include "../webdavcommon/webdav.h" #include "adaptorfactoryregistry.h" #include "applicationdomaintype.h" #include "domainadaptor.h" #include "eventpreprocessor.h" #include "todopreprocessor.h" #include "facade.h" #include "facadefactory.h" #include #include #define ENTITY_TYPE_EVENT "event" #define ENTITY_TYPE_TODO "todo" #define ENTITY_TYPE_CALENDAR "calendar" using Sink::ApplicationDomain::getTypeName; class CalDAVSynchronizer : public WebDavSynchronizer { using Event = Sink::ApplicationDomain::Event; using Todo = Sink::ApplicationDomain::Todo; using Calendar = Sink::ApplicationDomain::Calendar; public: explicit CalDAVSynchronizer(const Sink::ResourceContext &context) : WebDavSynchronizer(context, KDAV2::CalDav, getTypeName(), getTypeName()) { } protected: void updateLocalCollections(KDAV2::DavCollection::List calendarList) Q_DECL_OVERRIDE { SinkLog() << "Found" << calendarList.size() << "calendar(s)"; QVector ridList; for (const auto &remoteCalendar : calendarList) { const auto &rid = resourceID(remoteCalendar); SinkLog() << "Found calendar:" << remoteCalendar.displayName() << "[" << rid << "]"; Calendar localCalendar; localCalendar.setName(remoteCalendar.displayName()); localCalendar.setColor(remoteCalendar.color().name().toLatin1()); createOrModify(ENTITY_TYPE_CALENDAR, rid, localCalendar, {}); } } void updateLocalItem(KDAV2::DavItem remoteItem, const QByteArray &calendarLocalId) Q_DECL_OVERRIDE { const auto &rid = resourceID(remoteItem); auto ical = remoteItem.data(); auto incidence = KCalCore::ICalFormat().fromString(ical); using Type = KCalCore::IncidenceBase::IncidenceType; switch (incidence->type()) { case Type::TypeEvent: { Event localEvent; localEvent.setIcal(ical); localEvent.setCalendar(calendarLocalId); SinkTrace() << "Found an event with id:" << rid; createOrModify(ENTITY_TYPE_EVENT, rid, localEvent, {}); break; } case Type::TypeTodo: { Todo localTodo; localTodo.setIcal(ical); localTodo.setCalendar(calendarLocalId); SinkTrace() << "Found a Todo with id:" << rid; createOrModify(ENTITY_TYPE_TODO, rid, localTodo, {}); break; } case Type::TypeJournal: SinkWarning() << "Unimplemented add of a 'Journal' item in the Store"; break; case Type::TypeFreeBusy: SinkWarning() << "Unimplemented add of a 'FreeBusy' item in the Store"; break; case Type::TypeUnknown: SinkWarning() << "Trying to add a 'Unknown' item"; break; default: break; } } QByteArray collectionLocalResourceID(const KDAV2::DavCollection &calendar) Q_DECL_OVERRIDE { return syncStore().resolveRemoteId(ENTITY_TYPE_CALENDAR, resourceID(calendar)); } template KAsync::Job replayItem(const Item &localItem, Sink::Operation operation, const QByteArray &oldRemoteId, const QList &changedProperties, const QByteArray &entityType) { SinkLog() << "Replaying" << entityType; KDAV2::DavItem remoteItem; switch (operation) { case Sink::Operation_Creation: { auto rawIcal = localItem.getIcal(); if (rawIcal.isEmpty()) { return KAsync::error("No ICal in item for creation replay"); } auto collectionId = syncStore().resolveLocalId(ENTITY_TYPE_CALENDAR, localItem.getCalendar()); remoteItem.setData(rawIcal); remoteItem.setContentType("text/calendar"); remoteItem.setUrl(urlOf(collectionId, localItem.getUid())); SinkLog() << "Creating" << entityType << ":" << localItem.getSummary(); return createItem(remoteItem).then([remoteItem] { return resourceID(remoteItem); }); } case Sink::Operation_Removal: { // We only need the URL in the DAV item for removal remoteItem.setUrl(urlOf(oldRemoteId)); SinkLog() << "Removing" << entityType << ":" << oldRemoteId; return removeItem(remoteItem).then([] { return QByteArray{}; }); } case Sink::Operation_Modification: auto rawIcal = localItem.getIcal(); if (rawIcal.isEmpty()) { return KAsync::error("No ICal in item for modification replay"); } remoteItem.setData(rawIcal); remoteItem.setContentType("text/calendar"); remoteItem.setUrl(urlOf(oldRemoteId)); SinkLog() << "Modifying" << entityType << ":" << localItem.getSummary(); // It would be nice to check that the URL of the item hasn't // changed and move he item if it did, but since the URL is // pretty much arbitrary, whoe does that anyway? return modifyItem(remoteItem).then([oldRemoteId] { return oldRemoteId; }); } + return KAsync::null(); } KAsync::Job replay(const Event &event, Sink::Operation operation, const QByteArray &oldRemoteId, const QList &changedProperties) Q_DECL_OVERRIDE { return replayItem(event, operation, oldRemoteId, changedProperties, ENTITY_TYPE_EVENT); } KAsync::Job replay(const Todo &todo, Sink::Operation operation, const QByteArray &oldRemoteId, const QList &changedProperties) Q_DECL_OVERRIDE { return replayItem(todo, operation, oldRemoteId, changedProperties, ENTITY_TYPE_TODO); } KAsync::Job replay(const Calendar &calendar, Sink::Operation operation, const QByteArray &oldRemoteId, const QList &changedProperties) Q_DECL_OVERRIDE { SinkLog() << "Replaying calendar"; switch (operation) { case Sink::Operation_Creation: SinkWarning() << "Unimplemented replay of calendar creation"; break; case Sink::Operation_Removal: SinkLog() << "Replaying calendar removal"; removeCollection(urlOf(oldRemoteId)); break; case Sink::Operation_Modification: SinkWarning() << "Unimplemented replay of calendar modification"; break; } return KAsync::null(); } }; CalDavResource::CalDavResource(const Sink::ResourceContext &context) : Sink::GenericResource(context) { auto synchronizer = QSharedPointer::create(context); setupSynchronizer(synchronizer); setupPreprocessors(ENTITY_TYPE_EVENT, QVector() << new EventPropertyExtractor); setupPreprocessors(ENTITY_TYPE_TODO, QVector() << new TodoPropertyExtractor); } CalDavResourceFactory::CalDavResourceFactory(QObject *parent) : Sink::ResourceFactory(parent, { Sink::ApplicationDomain::ResourceCapabilities::Event::calendar, Sink::ApplicationDomain::ResourceCapabilities::Event::event, Sink::ApplicationDomain::ResourceCapabilities::Event::storage, Sink::ApplicationDomain::ResourceCapabilities::Todo::todo, Sink::ApplicationDomain::ResourceCapabilities::Todo::storage, }) { } Sink::Resource *CalDavResourceFactory::createResource(const Sink::ResourceContext &context) { return new CalDavResource(context); } using Sink::ApplicationDomain::Calendar; using Sink::ApplicationDomain::Event; using Sink::ApplicationDomain::Todo; void CalDavResourceFactory::registerFacades(const QByteArray &resourceName, Sink::FacadeFactory &factory) { factory.registerFacade>(resourceName); factory.registerFacade>(resourceName); factory.registerFacade>(resourceName); } void CalDavResourceFactory::registerAdaptorFactories( const QByteArray &resourceName, Sink::AdaptorFactoryRegistry ®istry) { registry.registerFactory>(resourceName); registry.registerFactory>(resourceName); registry.registerFactory>(resourceName); } void CalDavResourceFactory::removeDataFromDisk(const QByteArray &instanceIdentifier) { CalDavResource::removeFromDisk(instanceIdentifier); } diff --git a/examples/carddavresource/carddavresource.cpp b/examples/carddavresource/carddavresource.cpp index 916312fc..627f1458 100644 --- a/examples/carddavresource/carddavresource.cpp +++ b/examples/carddavresource/carddavresource.cpp @@ -1,182 +1,183 @@ /* * Copyright (C) 2015 Christian Mollekopf * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "carddavresource.h" #include "../webdavcommon/webdav.h" #include "facade.h" #include "resourceconfig.h" #include "log.h" #include "definitions.h" #include "synchronizer.h" #include "inspector.h" #include "facadefactory.h" #include "adaptorfactoryregistry.h" #include "contactpreprocessor.h" //This is the resources entity type, and not the domain type #define ENTITY_TYPE_CONTACT "contact" #define ENTITY_TYPE_ADDRESSBOOK "addressbook" using namespace Sink; class ContactSynchronizer : public WebDavSynchronizer { public: ContactSynchronizer(const Sink::ResourceContext &resourceContext) : WebDavSynchronizer(resourceContext, KDAV2::CardDav, ApplicationDomain::getTypeName(), ApplicationDomain::getTypeName()) {} QByteArray createAddressbook(const QString &addressbookName, const QString &addressbookPath, const QString &parentAddressbookRid) { SinkTrace() << "Creating addressbook: " << addressbookName << parentAddressbookRid; const auto remoteId = addressbookPath.toUtf8(); const auto bufferType = ENTITY_TYPE_ADDRESSBOOK; Sink::ApplicationDomain::Addressbook addressbook; addressbook.setName(addressbookName); if (!parentAddressbookRid.isEmpty()) { addressbook.setParent(syncStore().resolveRemoteId(ENTITY_TYPE_ADDRESSBOOK, parentAddressbookRid.toUtf8())); } createOrModify(bufferType, remoteId, addressbook, {}); return remoteId; } protected: void updateLocalCollections(KDAV2::DavCollection::List addressbookList) Q_DECL_OVERRIDE { const QByteArray bufferType = ENTITY_TYPE_ADDRESSBOOK; SinkTrace() << "Found" << addressbookList.size() << "addressbooks"; for (const auto &f : addressbookList) { const auto &rid = resourceID(f); SinkLog() << "Found addressbook:" << rid << f.displayName(); createAddressbook(f.displayName(), rid, ""); } } void updateLocalItem(KDAV2::DavItem remoteContact, const QByteArray &addressbookLocalId) Q_DECL_OVERRIDE { Sink::ApplicationDomain::Contact localContact; localContact.setVcard(remoteContact.data()); localContact.setAddressbook(addressbookLocalId); createOrModify(ENTITY_TYPE_CONTACT, resourceID(remoteContact), localContact, {}); } QByteArray collectionLocalResourceID(const KDAV2::DavCollection &addressbook) Q_DECL_OVERRIDE { return syncStore().resolveRemoteId(ENTITY_TYPE_ADDRESSBOOK, resourceID(addressbook)); } KAsync::Job replay(const ApplicationDomain::Contact &contact, Sink::Operation operation, const QByteArray &oldRemoteId, const QList &changedProperties) Q_DECL_OVERRIDE { SinkLog() << "Replaying to:" << serverUrl().url(); switch (operation) { case Sink::Operation_Creation: { const auto vcard = contact.getVcard(); if (vcard.isEmpty()) { return KAsync::error("No vcard in item for creation replay."); } auto collectionId = syncStore().resolveLocalId(ENTITY_TYPE_ADDRESSBOOK, contact.getAddressbook()); KDAV2::DavItem remoteItem; remoteItem.setData(vcard); remoteItem.setContentType("text/vcard"); remoteItem.setUrl(urlOf(collectionId, contact.getUid())); SinkLog() << "Creating:" << contact.getUid() << remoteItem.url().url() << vcard; return createItem(remoteItem).then([=] { return resourceID(remoteItem); }); } case Sink::Operation_Removal: { // We only need the URL in the DAV item for removal KDAV2::DavItem remoteItem; remoteItem.setUrl(urlOf(oldRemoteId)); SinkLog() << "Removing:" << oldRemoteId; return removeItem(remoteItem).then([] { return QByteArray{}; }); } case Sink::Operation_Modification: const auto vcard = contact.getVcard(); if (vcard.isEmpty()) { return KAsync::error("No ICal in item for modification replay"); } KDAV2::DavItem remoteItem; remoteItem.setData(vcard); remoteItem.setContentType("text/vcard"); remoteItem.setUrl(urlOf(oldRemoteId)); return modifyItem(remoteItem).then([=] { return oldRemoteId; }); } + return KAsync::null(); } KAsync::Job replay(const ApplicationDomain::Addressbook &addressbook, Sink::Operation operation, const QByteArray &oldRemoteId, const QList &changedProperties) Q_DECL_OVERRIDE { return KAsync::null(); } }; CardDavResource::CardDavResource(const Sink::ResourceContext &resourceContext) : Sink::GenericResource(resourceContext) { auto synchronizer = QSharedPointer::create(resourceContext); setupSynchronizer(synchronizer); setupPreprocessors(ENTITY_TYPE_CONTACT, QVector() << new ContactPropertyExtractor); } CardDavResourceFactory::CardDavResourceFactory(QObject *parent) : Sink::ResourceFactory(parent, {Sink::ApplicationDomain::ResourceCapabilities::Contact::contact, Sink::ApplicationDomain::ResourceCapabilities::Contact::addressbook, Sink::ApplicationDomain::ResourceCapabilities::Contact::storage } ) { } Sink::Resource *CardDavResourceFactory::createResource(const ResourceContext &context) { return new CardDavResource(context); } void CardDavResourceFactory::registerFacades(const QByteArray &name, Sink::FacadeFactory &factory) { factory.registerFacade>(name); factory.registerFacade>(name); } void CardDavResourceFactory::registerAdaptorFactories(const QByteArray &name, Sink::AdaptorFactoryRegistry ®istry) { registry.registerFactory>(name); registry.registerFactory>(name); } void CardDavResourceFactory::removeDataFromDisk(const QByteArray &instanceIdentifier) { CardDavResource::removeFromDisk(instanceIdentifier); } diff --git a/sinksh/syntax_modules/sink_livequery.cpp b/sinksh/syntax_modules/sink_livequery.cpp index 42d7d335..90a4ac72 100644 --- a/sinksh/syntax_modules/sink_livequery.cpp +++ b/sinksh/syntax_modules/sink_livequery.cpp @@ -1,144 +1,140 @@ /* * Copyright (C) 2014 Aaron Seigo * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include // tr() #include #include #include "common/resource.h" #include "common/storage.h" #include "common/resourceconfig.h" #include "common/log.h" #include "common/definitions.h" #include "common/store.h" #include "common/propertyparser.h" #include "sinksh_utils.h" #include "state.h" #include "syntaxtree.h" namespace SinkLiveQuery { Syntax::List syntax(); bool livequery(const QStringList &args_, State &state) { if (args_.isEmpty()) { state.printError(syntax()[0].usage()); return false; } auto options = SyntaxTree::parseOptions(args_); auto type = options.positionalArguments.isEmpty() ? QString{} : options.positionalArguments.first(); - bool asLine = true; - Sink::Query query; query.setId("livequery"); query.setFlags(Sink::Query::LiveQuery); if (!SinkshUtils::applyFilter(query, options)) { state.printError(syntax()[0].usage()); return false; } if (options.options.contains("resource")) { for (const auto &f : options.options.value("resource")) { query.resourceFilter(f.toLatin1()); } } if (options.options.contains("filter")) { for (const auto &f : options.options.value("filter")) { auto filter = f.split("="); const auto property = filter.value(0).toLatin1(); query.filter(property, Sink::PropertyParser::parse(type.toLatin1(), property, filter.value(1))); } } if (options.options.contains("id")) { for (const auto &f : options.options.value("id")) { query.filter(f.toUtf8()); } } // auto compact = options.options.contains("compact"); if (!options.options.contains("showall")) { if (options.options.contains("show")) { auto list = options.options.value("show"); std::transform(list.constBegin(), list.constEnd(), std::back_inserter(query.requestedProperties), [] (const QString &s) { return s.toLatin1(); }); } else { query.requestedProperties = SinkshUtils::requestedProperties(type); } - } else { - asLine = false; } QByteArrayList toPrint; QStringList tableLine; auto model = SinkshUtils::loadModel(query.type(), query); QObject::connect(model.data(), &QAbstractItemModel::dataChanged, [model, state](const QModelIndex &, const QModelIndex &, const QVector &roles) { if (roles.contains(Sink::Store::ChildrenFetchedRole)) { state.printLine(QObject::tr("Counted results %1").arg(model->rowCount(QModelIndex()))); } }); QObject::connect(model.data(), &QAbstractItemModel::rowsInserted, [model, state](const QModelIndex &index, int start, int end) { for (int i = start; i <= end; i++) { auto object = model->data(model->index(i, 0, index), Sink::Store::DomainObjectBaseRole).value(); state.printLine("Resource: " + object->resourceInstanceIdentifier(), 1); state.printLine("Identifier: " + object->identifier(), 1); state.stageTableLine(QStringList() << QObject::tr("Property:") << QObject::tr("Value:")); for (const auto &property : object->availableProperties()) { state.stageTableLine(QStringList() << property << object->getProperty(property).toString()); } state.flushTable(); } }); if (!model->data(QModelIndex(), Sink::Store::ChildrenFetchedRole).toBool()) { return true; } return false; } Syntax::List syntax() { Syntax list("livequery", QObject::tr("Run a livequery."), &SinkLiveQuery::livequery, Syntax::EventDriven); list.addPositionalArgument({"type", "The type to run the livequery on" }); list.addParameter("resource", {"resource", "Filter the livequery to the given resource" }); list.addFlag("compact", "Use a compact view (reduces the size of IDs)"); list.addParameter("filter", {"property=$value", "Filter the results" }); list.addParameter("id", {"id", "List only the content with the given ID" }); list.addFlag("showall", "Show all properties"); list.addParameter("show", {"property", "Only show the given property" }); list.completer = &SinkshUtils::resourceOrTypeCompleter; return Syntax::List() << list; } REGISTER_SYNTAX(SinkLiveQuery) }