From 67747899ac9b2404b30bd19dfcb4d334b38a6d07 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 31 May 2024 14:06:08 -0400 Subject: [PATCH] WIP on document conversion --- src/app-config.php | 2 + src/composer.json | 4 +- src/composer.lock | 14 +- src/lib/Data.php | 141 ++++++++++++-------- src/lib/Domain/Feed.php | 13 +- src/lib/Domain/Item.php | 3 + src/lib/Feed.php | 274 ++++++++++++++------------------------ src/lib/ItemAndFeed.php | 53 ++++++++ src/public/feed/index.php | 60 +++++---- src/util/refresh.php | 31 +++-- 10 files changed, 310 insertions(+), 285 deletions(-) create mode 100644 src/lib/ItemAndFeed.php diff --git a/src/app-config.php b/src/app-config.php index c06208c..f982c01 100644 --- a/src/app-config.php +++ b/src/app-config.php @@ -2,6 +2,7 @@ /** The current Feed Reader Central version */ +use BitBadger\Documents\SQLite\Configuration; use FeedReaderCentral\Data; const FRC_VERSION = '1.0.0-alpha7'; @@ -34,6 +35,7 @@ require __DIR__ . '/vendor/autoload.php'; require 'user-config.php'; +Configuration::useDbFileName(implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', DATABASE_NAME])); Data::ensureDb(); /** @var string The date the world wide web was created */ diff --git a/src/composer.json b/src/composer.json index 37557aa..96ef946 100644 --- a/src/composer.json +++ b/src/composer.json @@ -13,7 +13,9 @@ ], "require": { "bit-badger/documents-sqlite": "dev-conversion", - "ext-sqlite3": "*" + "ext-sqlite3": "*", + "ext-dom": "*", + "ext-curl": "*" }, "autoload": { "psr-4": { diff --git a/src/composer.lock b/src/composer.lock index 9dfa63d..f31827f 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d25fc7caa8f5a1ce4d45b8e111a481c7", + "content-hash": "6919c5b5b8f417396276d24c8f8edbde", "packages": [ { "name": "bit-badger/documents-common", @@ -12,10 +12,10 @@ "source": { "type": "git", "url": "https://git.bitbadger.solutions/bit-badger/documents-common", - "reference": "7372bbd4c297b9d79d982c7b7f5985e7bed7df51" + "reference": "60bf3a7d97f06d49db3cacb9a6a84b129a83daa6" }, "type": "library", - "time": "2024-05-30T02:56:55+00:00" + "time": "2024-05-31T16:06:59+00:00" }, { "name": "bit-badger/documents-sqlite", @@ -23,14 +23,14 @@ "source": { "type": "git", "url": "https://git.bitbadger.solutions/bit-badger/documents-sqlite", - "reference": "a5bae2dc4e6cf6c20d5b43aa721a5ca83cdf7c81" + "reference": "9378a62e7ac190ef4bbffdd4330bf83bbe39def0" }, "require": { "bit-badger/documents-common": "dev-conversion", "ext-sqlite3": "*" }, "type": "library", - "time": "2024-05-30T00:09:59+00:00" + "time": "2024-05-31T16:07:51+00:00" } ], "packages-dev": [], @@ -42,7 +42,9 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "ext-sqlite3": "*" + "ext-sqlite3": "*", + "ext-dom": "*", + "ext-curl": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/src/lib/Data.php b/src/lib/Data.php index 84c6665..d8ee22d 100644 --- a/src/lib/Data.php +++ b/src/lib/Data.php @@ -1,9 +1,15 @@ exec("CREATE VIRTUAL TABLE item_search USING fts5(content, content='item', content_rowid='id')"); - $db->exec(<<<'SQL' - CREATE TRIGGER item_ai AFTER INSERT ON item BEGIN - INSERT INTO item_search (rowid, content) VALUES (new.id, new.content); - END; - SQL); - $db->exec(<<<'SQL' - CREATE TRIGGER item_au AFTER UPDATE ON item BEGIN - INSERT INTO item_search (item_search, rowid, content) VALUES ('delete', old.id, old.content); - INSERT INTO item_search (rowid, content) VALUES (new.id, new.content); - END; - SQL); - $db->exec(<<<'SQL' - CREATE TRIGGER item_ad AFTER DELETE ON item BEGIN - INSERT INTO item_search (item_search, rowid, content) VALUES ('delete', old.id, old.content); - END; - SQL); + public static function createSearchIndex(SQLite3 $db): void + { + Custom::nonQuery("CREATE VIRTUAL TABLE item_search USING fts5(content, content='item', content_rowid='id')", + [], $db); + Custom::nonQuery(<<<'SQL' + CREATE TRIGGER item_ai AFTER INSERT ON item BEGIN + INSERT INTO item_search (rowid, content) VALUES (new.data->>'id', new.data->>'content'); + END; + SQL, [], $db); + Custom::nonQuery(<<<'SQL' + CREATE TRIGGER item_au AFTER UPDATE ON item BEGIN + INSERT INTO item_search ( + item_search, rowid, content + ) VALUES ( + 'delete', old.data->>'id', old.data->>'content' + ); + INSERT INTO item_search (rowid, content) VALUES (new.data->>'id', new.data->>'content'); + END; + SQL, [], $db); + Custom::nonQuery(<<<'SQL' + CREATE TRIGGER item_ad AFTER DELETE ON item BEGIN + INSERT INTO item_search ( + item_search, rowid, content + ) VALUES ( + 'delete', old.data->>'id', old.data->>'content' + ); + END; + SQL, [], $db); } /** * Make sure the expected tables exist + * + * @throws DocumentException If any is encountered */ - public static function ensureDb(): void { - $db = self::getConnection(); - $tables = array(); - $tableQuery = $db->query("SELECT name FROM sqlite_master WHERE type = 'table'"); - while ($table = $tableQuery->fetchArray(SQLITE3_NUM)) $tables[] = $table[0]; - if (!in_array('frc_user', $tables)) { - $db->exec(<<<'SQL' - CREATE TABLE frc_user ( - id INTEGER NOT NULL PRIMARY KEY, - email TEXT NOT NULL, - password TEXT NOT NULL) - SQL); - $db->exec('CREATE INDEX idx_user_email ON frc_user (email)'); + public static function ensureDb(): void + { + $tables = Custom::array("SELECT name FROM sqlite_master WHERE type = 'table'", [], new StringMapper('name')); + $db = Configuration::dbConn(); +// $tables = array(); +// $tableQuery = $db->query("SELECT name FROM sqlite_master WHERE type = 'table'"); +// while ($table = $tableQuery->fetchArray(SQLITE3_NUM)) $tables[] = $table[0]; + if (!in_array(Table::USER, $tables)) { + Definition::ensureTable(Table::USER, $db); + Definition::ensureFieldIndex(Table::USER, 'email', ['email'], $db); +// $db->exec(<<<'SQL' +// CREATE TABLE frc_user ( +// id INTEGER NOT NULL PRIMARY KEY, +// email TEXT NOT NULL, +// password TEXT NOT NULL) +// SQL); +// $db->exec('CREATE INDEX idx_user_email ON frc_user (email)'); } - if (!in_array('feed', $tables)) { - $db->exec(<<<'SQL' - CREATE TABLE feed ( - id INTEGER NOT NULL PRIMARY KEY, - user_id INTEGER NOT NULL, - url TEXT NOT NULL, - title TEXT, - updated_on TEXT, - checked_on TEXT, - FOREIGN KEY (user_id) REFERENCES frc_user (id)) - SQL); + if (!in_array(Table::FEED, $tables)) { + Definition::ensureTable(Table::FEED, $db); + Definition::ensureFieldIndex(Table::FEED, 'user', ['user_id'], $db); +// $db->exec(<<<'SQL' +// CREATE TABLE feed ( +// id INTEGER NOT NULL PRIMARY KEY, +// user_id INTEGER NOT NULL, +// url TEXT NOT NULL, +// title TEXT, +// updated_on TEXT, +// checked_on TEXT, +// FOREIGN KEY (user_id) REFERENCES frc_user (id)) +// SQL); } - if (!in_array('item', $tables)) { - $db->exec(<<<'SQL' - CREATE TABLE item ( - id INTEGER NOT NULL PRIMARY KEY, - feed_id INTEGER NOT NULL, - title TEXT NOT NULL, - item_guid TEXT NOT NULL, - item_link TEXT NOT NULL, - published_on TEXT NOT NULL, - updated_on TEXT, - content TEXT NOT NULL, - is_read BOOLEAN NOT NULL DEFAULT 0, - is_bookmarked BOOLEAN NOT NULL DEFAULT 0, - FOREIGN KEY (feed_id) REFERENCES feed (id)) - SQL); + if (!in_array(Table::ITEM, $tables)) { + Definition::ensureTable(Table::ITEM, $db); + Definition::ensureFieldIndex(Table::ITEM, 'feed', ['feed_id', 'item_link'], $db); +// $db->exec(<<<'SQL' +// CREATE TABLE item ( +// id INTEGER NOT NULL PRIMARY KEY, +// feed_id INTEGER NOT NULL, +// title TEXT NOT NULL, +// item_guid TEXT NOT NULL, +// item_link TEXT NOT NULL, +// published_on TEXT NOT NULL, +// updated_on TEXT, +// content TEXT NOT NULL, +// is_read BOOLEAN NOT NULL DEFAULT 0, +// is_bookmarked BOOLEAN NOT NULL DEFAULT 0, +// FOREIGN KEY (feed_id) REFERENCES feed (id)) +// SQL); self::createSearchIndex($db); } $db->close(); diff --git a/src/lib/Domain/Feed.php b/src/lib/Domain/Feed.php index f629fcc..bbf7734 100644 --- a/src/lib/Domain/Feed.php +++ b/src/lib/Domain/Feed.php @@ -4,6 +4,7 @@ namespace FeedReaderCentral\Domain; use DateTimeImmutable; use Exception; use FeedReaderCentral\Data; +use FeedReaderCentral\Feed as FeedParsed; use FeedReaderCentral\Key; /** @@ -54,15 +55,15 @@ class Feed /** * Create a document from the parsed feed * - * @param \FeedReaderCentral\Feed $feed The parsed feed + * @param FeedParsed $feed The parsed feed * @return static The document constructed from the parsed feed */ - public static function fromParsed(\FeedReaderCentral\Feed $feed): static + public static function fromParsed(FeedParsed $feed): static { - $it = new static(); - $it->user_id = $_SESSION[Key::USER_ID]; - $it->url = $feed->url; - $it->title = $feed->title; + $it = new static(); + $it->user_id = $_SESSION[Key::USER_ID]; + $it->url = $feed->url; + $it->title = $feed->title; $it->updated_on = $feed->updatedOn; $it->checked_on = Data::formatDate('now'); diff --git a/src/lib/Domain/Item.php b/src/lib/Domain/Item.php index e0c17d5..e718350 100644 --- a/src/lib/Domain/Item.php +++ b/src/lib/Domain/Item.php @@ -3,6 +3,9 @@ namespace FeedReaderCentral\Domain; use FeedReaderCentral\FeedItem; +/** + * An item from a feed + */ class Item { /** @var int The ID of this item in the Feed Reader Central database */ diff --git a/src/lib/Feed.php b/src/lib/Feed.php index 7324fff..799f568 100644 --- a/src/lib/Feed.php +++ b/src/lib/Feed.php @@ -1,24 +1,25 @@ patchFields(), $db); -// $query = $db->prepare(<<<'SQL' -// UPDATE item -// SET title = :title, -// published_on = :published, -// updated_on = :updated, -// content = :content, -// is_read = 0 -// WHERE id = :id -// SQL); -// $query->bindValue(':title', $item->title); -// $query->bindValue(':published', $item->publishedOn); -// $query->bindValue(':updated', $item->updatedOn); -// $query->bindValue(':content', $item->content); -// $query->bindValue(':id', $itemId); -// return $query->execute(); - return true; - } - - /** - * Add a feed item - * - * @param int $feedId The ID of the feed to which the item should be added - * @param FeedItem $item The item to be added - * @param SQLite3 $db A database connection to use for the addition - * @return bool|SQLite3Result The result if the update is successful, false if it failed - */ - private static function addItem(int $feedId, FeedItem $item, SQLite3 $db): bool|SQLite3Result { - Document::insert(Table::ITEM, Item::fromFeedItem($feedId, $item), $db); -// $query = $db->prepare(<<<'SQL' -// INSERT INTO item ( -// feed_id, item_guid, item_link, title, published_on, updated_on, content -// ) VALUES ( -// :feed, :guid, :link, :title, :published, :updated, :content -// ) -// SQL); -// $query->bindValue(':feed', $feedId); -// $query->bindValue(':guid', $item->guid); -// $query->bindValue(':link', $item->link); -// $query->bindValue(':title', $item->title); -// $query->bindValue(':published', $item->publishedOn); -// $query->bindValue(':updated', $item->updatedOn); -// $query->bindValue(':content', $item->content); -// return $query->execute(); - return true; - } - /** * Update a feed's items * @@ -345,24 +290,21 @@ class Feed { public static function updateItems(int $feedId, Feed $feed, DateTimeInterface $lastChecked, SQLite3 $db): array { $results = array_map(function ($item) use ($db, $feedId) { - // TODO: convert this query - $existsQuery = $db->prepare( - 'SELECT id, published_on, updated_on FROM item WHERE feed_id = :feed AND item_guid = :guid'); - $existsQuery->bindValue(':feed', $feedId); - $existsQuery->bindValue(':guid', $item->guid); - if ($exists = $existsQuery->execute()) { - if ($existing = $exists->fetchArray(SQLITE3_ASSOC)) { - if ( $existing['published_on'] != $item->publishedOn - || ($existing['updated_on'] ?? '') != ($item->updatedOn ?? '')) { - if (!self::updateItem($existing['id'], $item, $db)) return Data::error($db); + try { + $existing = Find::firstByFields(Table::ITEM, + [Field::EQ('feed_id', $feedId), Field::EQ('item_guid', $item->guid)], Item::class); + if ($existing) { + if ($existing->published_on != $item->publishedOn + || ($existing->updated_on != ($item->updatedOn ?? ''))) { + Patch::byId(Table::ITEM, $existing->id, $item->patchFields(), $db); } } else { - if (!self::addItem($feedId, $item, $db)) return Data::error($db); + Document::insert(Table::ITEM, Item::fromFeedItem($feedId, $item), $db); } - } else { - return Data::error($db); + return ['ok' => true]; + } catch (DocumentException $ex) { + return ['error' => "$ex"]; } - return ['ok' => true]; }, array_filter($feed->items, fn($it) => date_create_immutable($it->updatedOn ?? $it->publishedOn) >= $lastChecked)); $errors = array_map(fn($it) => $it['error'], array_filter($results, fn($it) => array_key_exists('error', $it))); @@ -381,26 +323,33 @@ class Feed { return ['error' => 'Unrecognized purge type ' . PURGE_TYPE]; } + $fields = [Field::EQ('feed_id', $feedId, '@feed'), Field::EQ('is_bookmarked', 0, '@book')]; + $sql = Query\Delete::byFields(Table::ITEM, $fields); + if (PURGE_TYPE == self::PURGE_READ) { + $readField = Field::EQ('is_read', 1, '@read'); + $fields[] = $readField; + $sql .= ' AND ' . Query::whereByFields([$readField]); + } elseif (PURGE_TYPE == self::PURGE_BY_DAYS) { + $fields[] = Field::EQ('', Data::formatDate('-' . PURGE_NUMBER . ' day'), '@oldest'); + $sql .= " AND date(coalesce(data->>'updated_on', data->>'published_on)) < date(@oldest)"; + } elseif (PURGE_TYPE == self::PURGE_BY_COUNT) { + $fields[] = Field::EQ('', PURGE_NUMBER, '@keep'); + $id = Configuration::idField(); + $table = Table::ITEM; + $sql .= ' ' . <<>'$id' IN ( + SELECT data->>'$id' FROM $table + WHERE data->>'feed_id' = @feed + ORDER BY date(coalesce(data->>'updated_on', data->>'published_on')) DESC + LIMIT -1 OFFSET @keep + ) + SQL; + } try { - // TODO: convert this query - $sql = match (PURGE_TYPE) { - self::PURGE_READ => 'AND is_read = 1', - self::PURGE_BY_DAYS => 'AND date(coalesce(updated_on, published_on)) < date(:oldest)', - self::PURGE_BY_COUNT => 'AND id IN (SELECT id FROM item WHERE feed_id = :feed - ORDER BY date(coalesce(updated_on, published_on)) DESC - LIMIT -1 OFFSET :keep)' - }; - - $purge = $db->prepare("DELETE FROM item WHERE feed_id = :feed AND is_bookmarked = 0 $sql"); - $purge->bindValue(':feed', $feedId); - if (PURGE_TYPE == self::PURGE_BY_DAYS) { - $purge->bindValue(':oldest', Data::formatDate('-' . PURGE_NUMBER . ' day')); - } elseif (PURGE_TYPE == self::PURGE_BY_COUNT) { - $purge->bindValue(':keep', PURGE_NUMBER); - } - return $purge->execute() ? ['ok' => true] : Data::error($db); - } catch (Exception $ex) { - return ['error' => $ex->getMessage()]; + Custom::nonQuery($sql, array_merge(array_map($it -> $it->asParameter(), $fields)), $db); + return ['ok' => true]; + } catch (DocumentException $ex) { + return ['error' => "$ex"]; } } @@ -417,35 +366,25 @@ class Feed { if (key_exists('error', $feedRetrieval)) return $feedRetrieval; $feed = $feedRetrieval['ok']; - $lastCheckedQuery = $db->prepare('SELECT checked_on FROM feed where id = :id'); - $lastCheckedQuery->bindValue(':id', $feedId); - if (!($lastCheckedResult = $lastCheckedQuery->execute())) return Data::error($db); - if (!($lastChecked = date_create_immutable($lastCheckedResult->fetchArray(SQLITE3_NUM)[0] ?? WWW_EPOCH))) { - return ['error' => 'Could not derive date last checked for feed']; + try { + $feedDoc = Find::byId(Table::FEED, $feedId, FeedDocument::class); + if (!$feedDoc) return ['error' => 'Could not derive date last checked for feed']; + $lastChecked = date_create_immutable($feedDoc->checked_on ?? WWW_EPOCH); + + $itemUpdate = self::updateItems($feedId, $feed, $lastChecked, $db); + if (key_exists('error', $itemUpdate)) return $itemUpdate; + + $patch = [ + 'title' => $feed->title, + 'updated_on' => $feed->updatedOn, + 'checked_on' => Data::formatDate('now') + ]; + if ($url == $feed->url) $patch['url'] = $feed->url; + Patch::byId(Table::FEED, $feedId, $patch, $db); + } catch (DocumentException $ex) { + return ['error' => "$ex"]; } - $itemUpdate = self::updateItems($feedId, $feed, $lastChecked, $db); - if (key_exists('error', $itemUpdate)) return $itemUpdate; - - $patch = ['title' => $feed->title, 'updated_on' => $feed->updatedOn, 'checked_on' => Data::formatDate('now')]; - if ($url == $feed->url) $patch['url'] = $feed->url; - Patch::byId(Table::FEED, $feedId, $patch, $db); -// $urlUpdate = $url == $feed->url ? '' : ', url = :url'; -// $feedUpdate = $db->prepare(<<bindValue(':title', $feed->title); -// $feedUpdate->bindValue(':updated', $feed->updatedOn); -// $feedUpdate->bindValue(':checked', Data::formatDate('now')); -// $feedUpdate->bindValue(':id', $feedId); -// if ($urlUpdate != '') $feedUpdate->bindValue(':url', $feed->url); -// if (!$feedUpdate->execute()) return Data::error($db); - return PURGE_TYPE == self::PURGE_NONE ? ['ok' => true] : self::purgeItems($feedId, $db); } @@ -461,31 +400,19 @@ class Feed { $feed = $feedExtract['ok']; - $whereUserAndUrl = ' WHERE ' . Query::whereByField(Field::EQ('user_id', ''), '@user') - . ' AND ' . Query::whereByField(Field::EQ('url', ''), '@url'); - $userAndUrlParams = ['@user' => $_SESSION[Key::USER_ID], '@url' => $feed->url]; - if (Custom::scalar('SELECT EXISTS (SELECT 1 FROM ' . Table::FEED . $whereUserAndUrl . ')', $userAndUrlParams, - Results::toExists(...), $db)) { - return ['error' => "Already subscribed to feed $feed->url"]; - } + try { + $fields = [Field::EQ('user_id', $_SESSION[Key::USER_ID]), Field::EQ('url', $feed->url)]; + if (Exists::byFields(Table::FEED, $fields, $db)) { + return ['error' => "Already subscribed to feed $feed->url"]; + } - Document::insert(Table::FEED, FeedDocument::fromParsed($feed), $db); -// $query = $db->prepare(<<<'SQL' -// INSERT INTO feed ( -// user_id, url, title, updated_on, checked_on -// ) VALUES ( -// :user, :url, :title, :updated, :checked -// ) -// SQL); -// $query->bindValue(':user', $_SESSION[Key::USER_ID]); -// $query->bindValue(':url', $feed->url); -// $query->bindValue(':title', $feed->title); -// $query->bindValue(':updated', $feed->updatedOn); -// $query->bindValue(':checked', Data::formatDate('now')); -// if (!$query->execute()) return Data::error($db); - $doc = Custom::single(Query::selectFromTable(Table::FEED) . $whereUserAndUrl, $userAndUrlParams, - Results::fromData(...), Domain\Feed::class, $db); - if (!$doc) return ['error' => 'Could not retrieve inserted feed']; + Document::insert(Table::FEED, FeedDocument::fromParsed($feed), $db); + + $doc = Find::firstByFields(Table::FEED, $fields, FeedDocument::class); + if (!$doc) return ['error' => 'Could not retrieve inserted feed']; + } catch (DocumentException $ex) { + return ['error' => "$ex"]; + } $result = self::updateItems($doc->id, $feed, date_create_immutable(WWW_EPOCH), $db); if (key_exists('error', $result)) return $result; @@ -502,12 +429,13 @@ class Feed { * @return bool[]|string[] [ 'ok' => true ] if successful, [ 'error' => message ] if not */ public static function update(FeedDocument $existing, string $url, SQLite3 $db): array { - // TODO: convert this query (need to make Query\Patch::update visible) - $query = $db->prepare('UPDATE feed SET url = :url WHERE id = :id AND user_id = :user'); - $query->bindValue(':url', $url); - $query->bindValue(':id', $existing->id); - $query->bindValue(':user', $_SESSION[Key::USER_ID]); - if (!$query->execute()) return Data::error($db); + try { + Patch::byFields(Table::FEED, + [Field::EQ(Configuration::idField(), $existing->id), Field::EQ('user_id', $_SESSION[Key::USER_ID])], + ['url' => $url], $db); + } catch (DocumentException $ex) { + return ['error' => "$ex"]; + } return self::refreshFeed($existing->id, $url, $db); } @@ -515,19 +443,15 @@ class Feed { /** * Retrieve all feeds, optionally for a specific user * - * @param SQLite3 $db The database connection to use to retrieve the feeds * @param int $user The ID of the user whose feeds should be retrieved (optional, defaults to all feeds) - * @return array An array of arrays with ['id', 'url', 'email'] keys + * @return DocumentList A list of feeds + * @throws DocumentException If any is encountered */ - public static function retrieveAll(SQLite3 $db, int $user = 0): array { - $extraSQL = $user > 0 ? ' WHERE u.id = :user' : ''; - $query = $db->prepare( - "SELECT f.id, f.url, u.email FROM feed f INNER JOIN frc_user u ON u.id = f.user_id$extraSQL"); - if ($user > 0) $query->bindValue(':user', $user); - if (!($result = $query->execute())) return Data::error($db); - $feeds = []; - while ($feed = $result->fetchArray(SQLITE3_ASSOC)) $feeds[] = $feed; - return $feeds; + public static function retrieveAll(int $user = 0): DocumentList + { + return $user == 0 + ? Find::all(Table::FEED, FeedDocument::class) + : Find::byFields(Table::FEED, [Field::EQ('user_id', $user)], FeedDocument::class); } /** @@ -537,15 +461,19 @@ class Feed { * @return array|true[]|string[] ['ok' => true] if successful, * ['error' => message] if not (may have multiple error lines) */ - public static function refreshAll(SQLite3 $db): array { - $feeds = self::retrieveAll($db, $_SESSION[Key::USER_ID]); - if (key_exists('error', $feeds)) return $feeds; + public static function refreshAll(SQLite3 $db): array + { + try { + $feeds = self::retrieveAll($_SESSION[Key::USER_ID]); - $errors = []; - array_walk($feeds, function ($feed) use ($db, &$errors) { - $result = self::refreshFeed($feed['id'], $feed['url'], $db); - if (key_exists('error', $result)) $errors[] = $result['error']; - }); + $errors = []; + foreach ($feeds->items() as $feed) { + $result = self::refreshFeed($feed->id, $feed->url, $db); + if (key_exists('error', $result)) $errors[] = $result['error']; + } + } catch (DocumentException $ex) { + return ['error' => "$ex"]; + } return sizeof($errors) == 0 ? ['ok' => true] : ['error' => implode("\n", $errors)]; } @@ -554,15 +482,11 @@ class Feed { * Retrieve a feed by its ID for the current user * * @param int $feedId The ID of the feed to retrieve - * @param SQLite3 $db A database connection to use to retrieve the feed * @return FeedDocument|false The data for the feed if found, false if not found + * @throws DocumentException If any is encountered */ - public static function retrieveById(int $feedId, SQLite3 $db): FeedDocument|false { - $doc = Find::byId(Table::FEED, $feedId, FeedDocument::class, $db); + public static function retrieveById(int $feedId): FeedDocument|false { + $doc = Find::byId(Table::FEED, $feedId, FeedDocument::class); return $doc && $doc->user_id == $_SESSION[Key::USER_ID] ? $doc : false; -// $query = $db->prepare('SELECT * FROM feed WHERE id = :id AND user_id = :user'); -// $query->bindValue(':id', $feedId); -// $query->bindValue(':user', $_SESSION[Key::USER_ID]); -// return ($result = $query->execute()) ? $result->fetchArray(SQLITE3_ASSOC) : false; } } diff --git a/src/lib/ItemAndFeed.php b/src/lib/ItemAndFeed.php new file mode 100644 index 0000000..e5807e8 --- /dev/null +++ b/src/lib/ItemAndFeed.php @@ -0,0 +1,53 @@ + A mapper to deserialize this from the query + */ + public static function mapper(): Mapper + { + return new class implements Mapper { + public function map(array $result): ItemAndFeed + { + $it = new ItemAndFeed(); + $it->item = (new JsonMapper(Item::class, 'item_data'))->map($result); + $it->feed = (new JsonMapper(Feed::class, 'feed_data'))->map($result); + return $it; + } + }; + } + + /** + * Generate the `SELECT` and `FROM` clauses for the query to retrieve this item + * + * @return string The `SELECT` and `FROM` clauses to retrieve these items + */ + public static function selectFrom(): string + { + $item = Table::ITEM; + $feed = Table::FEED; + return <<>'feed_id' = $feed.data->>'id' + SQL; + } +} diff --git a/src/public/feed/index.php b/src/public/feed/index.php index 462b1aa..00e40fe 100644 --- a/src/public/feed/index.php +++ b/src/public/feed/index.php @@ -5,6 +5,7 @@ * Allows users to add, edit, and delete feeds */ +use BitBadger\Documents\DocumentException; use BitBadger\Documents\Field; use BitBadger\Documents\SQLite\Delete; use FeedReaderCentral\Data; @@ -20,46 +21,49 @@ Security::verifyUser($db); $feedId = $_GET['id'] ?? ''; if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { - if (!($feed = Feed::retrieveById($feedId, $db))) not_found(); - Delete::byField(Table::ITEM, Field::EQ('feed_id', $feed->id), $db); -// $itemDelete = $db->prepare('DELETE FROM item WHERE feed_id = :feed'); -// $itemDelete->bindValue(':feed', $feed['id']); -// if (!$itemDelete->execute()) add_error(Data::error($db)['error']); - Delete::byId(Table::FEED, $feed->id, $db); -// $feedDelete = $db->prepare('DELETE FROM feed WHERE id = :feed'); -// $feedDelete->bindValue(':feed', $feed['id']); -// if ($feedDelete->execute()) { - add_info('Feed “' . htmlentities($feed['title']) . '” deleted successfully'); -// } else { -// add_error(Data::error($db)['error']); -// } - frc_redirect('/feeds'); + try { + if (!($feed = Feed::retrieveById($feedId))) not_found(); + Delete::byFields(Table::ITEM, [Field::EQ('feed_id', $feed->id)], $db); + Delete::byId(Table::FEED, $feed->id, $db); + add_info('Feed “' . htmlentities($feed->title) . '” deleted successfully'); + $db->close(); + frc_redirect('/feeds'); + } catch (DocumentException $ex) { + add_error("$ex"); + } } if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $isNew = $_POST['id'] == 'new'; - if ($isNew) { - $result = Feed::add($_POST['url'], $db); - } else { - $toEdit = Feed::retrieveById($_POST['id'], $db); - $result = $toEdit ? Feed::update($toEdit, $_POST['url'], $db) : ['error' => "Feed {$_POST['id']} not found"]; + try { + $isNew = $_POST['id'] == 'new'; + if ($isNew) { + $result = Feed::add($_POST['url'], $db); + } else { + $toEdit = Feed::retrieveById($_POST['id']); + $result = $toEdit + ? Feed::update($toEdit, $_POST['url'], $db) + : ['error' => "Feed {$_POST['id']} not found"]; + } + if (key_exists('ok', $result)) { + add_info('Feed saved successfully'); + $db->close(); + frc_redirect('/feeds'); + } + add_error($result['error']); + $feedId = 'error'; + } catch (DocumentException $ex) { + add_error("$ex"); } - if (key_exists('ok', $result)) { - add_info('Feed saved successfully'); - frc_redirect('/feeds'); - } - add_error($result['error']); - $feedId = 'error'; } if ($feedId == 'new') { $title = 'Add RSS Feed'; - $feed = [ 'id' => $_GET['id'], 'url' => '']; + $feed = ['id' => $_GET['id'], 'url' => '']; } else { $title = 'Edit RSS Feed'; if ($feedId == 'error') { $feed = ['id' => $_POST['id'] ?? '', 'url' => $_POST['url'] ?? '']; - } elseif (!($feed = Feed::retrieveById((int) $feedId, $db))) not_found(); + } elseif (!($feed = Feed::retrieveById((int)$feedId))) not_found(); } page_head($title); ?> diff --git a/src/util/refresh.php b/src/util/refresh.php index e2ca0bb..0e34d5f 100644 --- a/src/util/refresh.php +++ b/src/util/refresh.php @@ -1,6 +1,10 @@ items() as /** @var Feed $feed */ $feed) { + $result = Feed::refreshFeed($feed->id, $feed->url, $db); + $userKey = "$feed->user_id"; + if (!key_exists($userKey, $users)) $users[$userKey] = Find::byId(Table::USER, $feed->user_id, User::class); if (array_key_exists('error', $result)) { - printfn('ERR (%s) %s', $feed['email'], $feed['url']); + printfn('ERR (%s) %s', $users[$userKey]->email, $feed->url); printfn(' %s', $result['error']); } else { - printfn('OK (%s) %s', $feed['email'], $feed['url']); + printfn('OK (%s) %s', $users[$userKey]->email, $feed->url); } - }); + } printfn(PHP_EOL . 'All feeds refreshed'); - } finally { + } catch (DocumentException $ex) { + printfn("ERR $ex"); + return; + } + finally { $db->close(); } }