Documents and Documentation (beta 1) (#23)

- Change to SQLite document store
- Complete documentation on usage of Feed Reader Central
- Update INSTALLING.md for new installation procedures

Reviewed-on: #23
This commit was merged in pull request #23.
This commit is contained in:
2024-06-12 02:07:35 +00:00
parent 819979f2b2
commit 0c87392910
43 changed files with 1652 additions and 1142 deletions
+110
View File
@@ -0,0 +1,110 @@
<?php declare(strict_types=1);
use BitBadger\PDODocument\{Custom, Document, DocumentException};
use BitBadger\PDODocument\Mapper\{ArrayMapper, ExistsMapper};
use FeedReaderCentral\{Data, Feed, Item, Table, User};
require __DIR__ . '/../cli-start.php';
cli_title('DATABASE UPDATE');
//const PDO_DOC_DEBUG_SQL = true;
if ($argc < 2) display_help();
switch ($argv[1]) {
case 'check':
check_status();
break;
case 'run':
run_update();
break;
default:
printfn('Unrecognized option "%s"', $argv[1]);
display_help();
}
/**
* Display the options for this utility and exit
*/
function display_help(): never
{
printfn('Options:');
printfn(' - check');
printfn(' Check to see if the configured database has been updated');
printfn(' - run');
printfn(' Run the beta1 database storage update');
exit(0);
}
function json_column_exists(): bool
{
try {
$table = Custom::single("SELECT sql FROM sqlite_master WHERE tbl_name='frc_user'", [], new ArrayMapper());
return $table && substr_compare(strtolower($table['sql']), 'data text not null', 0) >= 0;
} catch (DocumentException $ex) {
printfn("ERR $ex");
return false;
}
}
function check_status(): void
{
if (json_column_exists()) {
printfn('The database has already been updated');
} else {
printfn('The database has yet to be updated');
}
}
function run_update(): void
{
try {
$searchExists = Custom::scalar("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = 'item_ai')", [],
new ExistsMapper());
if ($searchExists) {
printfn('Removing search index...');
Custom::nonQuery('DROP TRIGGER item_ai', []);
Custom::nonQuery('DROP TRIGGER item_au', []);
Custom::nonQuery('DROP TRIGGER item_ad', []);
Custom::nonQuery('DROP TABLE item_search', []);
}
printfn('Moving old tables...');
Custom::nonQuery('ALTER TABLE item RENAME TO old_item', []);
Custom::nonQuery('ALTER TABLE feed RENAME TO old_feed', []);
Custom::nonQuery('ALTER TABLE frc_user RENAME TO old_user', []);
printfn('Creating new tables...');
Data::ensureDb();
printfn('Migrating users...');
$users = Custom::list('SELECT * FROM old_user', [], new ArrayMapper());
if (!$users->hasItems()) throw new DocumentException('Could not retrieve users');
foreach ($users->items() as $user) {
Document::insert(Table::USER, new User($user['id'], $user['email'], $user['password']));
}
printfn('Migrating feeds...');
$feeds = Custom::list('SELECT * FROM old_feed', [], new ArrayMapper());
if (!$feeds->hasItems()) throw new DocumentException('Could not retrieve feeds');
foreach ($feeds->items() as $feed) {
Document::insert(Table::FEED,
new Feed($feed['id'], $feed['user_id'], $feed['url'], $feed['title'], $feed['updated_on'],
$feed['checked_on']));
}
printfn('Migrating items...');
$items = Custom::list('SELECT * FROM old_item', [], new ArrayMapper());
if (!$items->hasItems()) throw new DocumentException('Could not retrieve items');
foreach ($items->items() as $item) {
Document::insert(Table::ITEM,
new Item($item['id'], $item['feed_id'], $item['title'], $item['item_guid'], $item['item_link'],
$item['published_on'], $item['updated_on'], $item['content'], $item['is_read'],
$item['is_bookmarked']));
}
printfn('Dropping old tables...');
Custom::nonQuery('DROP TABLE old_item', []);
Custom::nonQuery('DROP TABLE old_feed', []);
Custom::nonQuery('DROP TABLE old_user', []);
printfn(PHP_EOL. 'Migration complete!');
} catch (DocumentException $ex) {
printfn("ERR $ex");
}
}
+18 -18
View File
@@ -1,5 +1,7 @@
<?php
use JetBrains\PhpStorm\NoReturn;
<?php declare(strict_types=1);
use BitBadger\PDODocument\{DocumentException, Find};
use FeedReaderCentral\{Feed, Table, User};
require __DIR__ . '/../cli-start.php';
@@ -20,34 +22,32 @@ switch ($argv[1]) {
/**
* Display the options for this utility and exit
*/
#[NoReturn]
function display_help(): void {
function display_help(): never
{
printfn('Options:');
printfn(' - all');
printfn(' Refreshes all feeds');
exit(0);
}
function refresh_all(): void {
$db = Data::getConnection();
function refresh_all(): void
{
try {
$feeds = Feed::retrieveAll($db);
if (array_key_exists('error', $feeds)) {
printfn('SQLite error: %s', $feeds['error']);
return;
}
array_walk($feeds, function ($feed) use ($db) {
$result = Feed::refreshFeed($feed['id'], $feed['url'], $db);
$users = [];
iterator_apply(Feed::retrieveAll()->items(), function (Feed $feed) use (&$users) {
$result = Feed::refreshFeed($feed->id, $feed->url);
$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 {
$db->close();
} catch (DocumentException $ex) {
printfn("ERR $ex");
return;
}
}
+16 -13
View File
@@ -1,5 +1,8 @@
<?php
use JetBrains\PhpStorm\NoReturn;
<?php declare(strict_types=1);
use BitBadger\PDODocument\{Custom, DocumentException};
use BitBadger\PDODocument\Mapper\ExistsMapper;
use FeedReaderCentral\Data;
require __DIR__ . '/../cli-start.php';
@@ -20,8 +23,8 @@ switch ($argv[1]) {
/**
* Display the options for this utility and exit
*/
#[NoReturn]
function display_help(): void {
function display_help(): never
{
printfn('Options:');
printfn(' - rebuild');
printfn(' Rebuilds search index');
@@ -31,19 +34,19 @@ function display_help(): void {
/**
* Rebuild the search index, creating it if it does not already exist
*/
function rebuild_index(): void {
$db = Data::getConnection();
function rebuild_index(): void
{
try {
$hasIndex = $db->query("SELECT COUNT(*) FROM sqlite_master WHERE name = 'item_ai'");
if ($hasIndex->fetchArray(SQLITE3_NUM)[0] == 0) {
$hasIndex = Custom::scalar("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = 'item_ai')", [],
new ExistsMapper());
if (!$hasIndex) {
printfn('Creating search index....');
Data::createSearchIndex($db);
Data::createSearchIndex();
}
printfn('Rebuilding search index...');
$db->exec("INSERT INTO item_search (item_search) VALUES ('rebuild')");
Custom::nonQuery("INSERT INTO item_search (item_search) VALUES ('rebuild')", []);
printfn(PHP_EOL . 'Search index rebuilt');
} finally {
$db->close();
} catch (DocumentException $ex) {
printfn("$ex");
}
}
+49 -57
View File
@@ -1,5 +1,7 @@
<?php
use JetBrains\PhpStorm\NoReturn;
<?php declare(strict_types=1);
use BitBadger\PDODocument\{Count, Custom, Delete, DocumentException, Field, Parameters, Patch, Query};
use FeedReaderCentral\{Security, Table, User};
require __DIR__ . '/../cli-start.php';
@@ -58,8 +60,8 @@ switch ($argv[1]) {
/**
* Display the options for this utility and exit
*/
#[NoReturn]
function display_help(): void {
function display_help(): never
{
printfn('Options:');
printfn(' - add-user [e-mail] [password]');
printfn(' Adds a new user to this instance');
@@ -83,24 +85,23 @@ function display_help(): void {
/**
* Add a new user
*/
function add_user(): void {
function add_user(): void
{
global $argv;
$db = Data::getConnection();
try {
// Ensure there is not already a user with this e-mail address
$user = Security::findUserByEmail($argv[2], $db);
$user = User::findByEmail($argv[2]);
if ($user) {
printfn('A user with e-mail address "%s" already exists', $argv[2]);
return;
}
Security::addUser($argv[2], $argv[3], $db);
User::add($argv[2], $argv[3]);
printfn('User "%s" with password "%s" added successfully', $argv[2], $argv[3]);
} finally {
$db->close();
} catch (DocumentException $ex) {
printfn("$ex");
}
}
@@ -110,32 +111,33 @@ function add_user(): void {
* @param string $email The e-mail address of the user
* @return string The string to use when displaying results
*/
function display_user(string $email): string {
function display_user(string $email): string
{
return $email == Security::SINGLE_USER_EMAIL ? 'single-user mode user' : "user \"$email\"";
}
/**
* Set a user's password
*/
function set_password(string $email, string $password): void {
$db = Data::getConnection();
function set_password(string $email, string $password): void
{
try {
$displayUser = display_user($email);
// Ensure this user exists
$user = Security::findUserByEmail($email, $db);
$user = User::findByEmail($email);
if (!$user) {
printfn('No %s exists', $displayUser);
return;
}
Security::updatePassword($email, $password, $db);
Security::updatePassword($email, $password);
$msg = $email == Security::SINGLE_USER_EMAIL && $password == Security::SINGLE_USER_PASSWORD
? 'reset' : sprintf('set to "%s"', $password);
printfn('%s password %s successfully', init_cap($displayUser), $msg);
} finally {
$db->close();
} catch (DocumentException $ex) {
printfn("$ex");
}
}
@@ -144,76 +146,66 @@ function set_password(string $email, string $password): void {
*
* @param string $email The e-mail address of the user to be deleted
*/
function delete_user(string $email): void {
$db = Data::getConnection();
function delete_user(string $email): void
{
try {
$displayUser = display_user($email);
// Get the ID for the provided e-mail address
$user = Security::findUserByEmail($email, $db);
// Get the user for the provided e-mail address
$user = User::findByEmail($email);
if (!$user) {
printfn('No %s exists', $displayUser);
return;
}
$feedCountQuery = $db->prepare('SELECT COUNT(*) FROM feed WHERE user_id = :user');
$feedCountQuery->bindValue(':user', $user['id']);
$feedCountResult = $feedCountQuery->execute();
if (!$feedCountResult) {
printfn('SQLite error: %s', $db->lastErrorMsg());
try {
$feedCount = Count::byFields(Table::FEED, [Field::EQ('user_id', $user->id)]);
} catch (DocumentException $ex) {
printfn("$ex");
return;
}
$feedCount = $feedCountResult->fetchArray(SQLITE3_NUM);
$proceed = readline("Delete the $displayUser and their $feedCount[0] feed(s)? (y/N)" . PHP_EOL);
$proceed = readline("Delete the $displayUser and their $feedCount feed(s)? (y/N)" . PHP_EOL);
if (!$proceed || !str_starts_with(strtolower($proceed), 'y')) {
printfn('Deletion canceled');
return;
}
$itemDelete = $db->prepare('DELETE FROM item WHERE feed_id IN (SELECT id FROM feed WHERE user_id = :user)');
$itemDelete->bindValue(':user', $user['id']);
$itemDelete->execute();
try {
$fields = [Field::EQ('user_id', $user->id, '@user')];
Custom::nonQuery(
'DELETE FROM ' . Table::ITEM . " WHERE data->>'feed_id' IN (SELECT data->>'id' FROM " . Table::FEED
. ' WHERE ' . Query::whereByFields($fields) . ')', Parameters::addFields($fields, []));
Delete::byFields(Table::FEED, $fields);
Delete::byId(Table::USER, $user->id);
$feedDelete = $db->prepare('DELETE FROM feed WHERE user_id = :user');
$feedDelete->bindValue(':user', $user['id']);
$feedDelete->execute();
$userDelete = $db->prepare('DELETE FROM frc_user WHERE id = :user');
$userDelete->bindValue(':user', $user['id']);
$userDelete->execute();
printfn('%s deleted successfully', init_cap($displayUser));
} finally {
$db->close();
printfn('%s deleted successfully', init_cap($displayUser));
} catch (DocumentException $ex) {
printfn("$ex");
}
} catch (DocumentException $ex) {
printfn("$ex");
}
}
/**
* Change the single-user mode user to a different e-mail address and password
*/
function migrate_single_user(): void {
function migrate_single_user(): void
{
global $argv;
$db = Data::getConnection();
try {
$single = Security::findUserByEmail(Security::SINGLE_USER_EMAIL, $db);
if (!$single) {
if (!$single = User::findByEmail(Security::SINGLE_USER_EMAIL)) {
printfn('There is no single-user mode user to be migrated');
return;
}
$migrateQuery = $db->prepare('UPDATE frc_user SET email = :email, password = :password WHERE id = :id');
$migrateQuery->bindValue(':email', $argv[2]);
$migrateQuery->bindValue(':password', password_hash($argv[3], Security::PW_ALGORITHM));
$migrateQuery->bindValue(':id', $single['id']);
$migrateQuery->execute();
Patch::byId(Table::USER, $single->id,
['email' => $argv[2], 'password' => password_hash($argv[3], Security::PW_ALGORITHM)]);
printfn('The single user has been moved to "%s", with password "%s"', $argv[2], $argv[3]);
} finally {
$db->close();
} catch (DocumentException $ex) {
printfn("$ex");
}
}