WIP on PHP (Leaf) version
This commit is contained in:
		
							parent
							
								
									3df5c71d81
								
							
						
					
					
						commit
						0ec4fd017f
					
				
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -254,3 +254,6 @@ paket-files/
 | 
			
		||||
 | 
			
		||||
# Ionide VSCode extension
 | 
			
		||||
.ionide
 | 
			
		||||
 | 
			
		||||
# in-progress: PHP version
 | 
			
		||||
src/app/vendor
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								src/app/.htaccess
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/app/.htaccess
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
RewriteEngine on
 | 
			
		||||
RewriteCond %{REQUEST_FILENAME} !-d
 | 
			
		||||
RewriteCond %{REQUEST_FILENAME} !-f
 | 
			
		||||
RewriteRule . index.php [L]
 | 
			
		||||
							
								
								
									
										135
									
								
								src/app/Data.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/app/Data.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,135 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace MyPrayerJournal;
 | 
			
		||||
 | 
			
		||||
use BitBadger\PgSQL\Documents\{ Configuration, Definition, Document, DocumentIndex, Query };
 | 
			
		||||
use MyPrayerJournal\Domain\{ History, JournalRequest, Note, Request, RequestAction };
 | 
			
		||||
 | 
			
		||||
class Data
 | 
			
		||||
{
 | 
			
		||||
    /** The prayer request table */
 | 
			
		||||
    const REQ_TABLE = 'prayer_request';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Ensure the table and index exist
 | 
			
		||||
     */
 | 
			
		||||
    public static function startUp()
 | 
			
		||||
    {
 | 
			
		||||
        Configuration::$connectionString = "pgsql:host=localhost;port=5432;dbname=leafjson;user=leaf;password=leaf";
 | 
			
		||||
        Definition::ensureTable(Data::REQ_TABLE);
 | 
			
		||||
        Definition::ensureIndex(Data::REQ_TABLE, DocumentIndex::Optimized);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find a full prayer request by its ID
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $reqId The request ID
 | 
			
		||||
     * @param string $userId The ID of the currently logged-on user
 | 
			
		||||
     * @return ?Request The request, or null if it is not found
 | 
			
		||||
     */
 | 
			
		||||
    public static function findFullRequestById(string $reqId, string $userId): ?Request
 | 
			
		||||
    {
 | 
			
		||||
        $req = Document::findById(Data::REQ_TABLE, $reqId, Request::class);
 | 
			
		||||
        return is_null($req) || $req->userId != $userId ? null : $req;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a history entry to the specified request
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $reqId The request ID
 | 
			
		||||
     * @param string $userId The ID of the currently logged-on user
 | 
			
		||||
     * @param History $history The history entry to be added
 | 
			
		||||
     */
 | 
			
		||||
    public static function addHistory(string $reqId, string $userId, History $history)
 | 
			
		||||
    {
 | 
			
		||||
        $req = Data::findFullRequestById($reqId, $userId);
 | 
			
		||||
        if (is_null($req)) throw new \InvalidArgumentException("$reqId not found");
 | 
			
		||||
        array_unshift($req->history, $history);
 | 
			
		||||
        Document::updateFull(Data::REQ_TABLE, $reqId, $req);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a note to the specified request
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $reqId The request ID
 | 
			
		||||
     * @param string $userId The ID of the currently logged-on user
 | 
			
		||||
     * @param Note $note The note to be added
 | 
			
		||||
     */
 | 
			
		||||
    public static function addNote(string $reqId, string $userId, Note $note)
 | 
			
		||||
    {
 | 
			
		||||
        $req = Data::findFullRequestById($reqId, $userId);
 | 
			
		||||
        if (is_null($req)) throw new \InvalidArgumentException("$reqId not found");
 | 
			
		||||
        array_unshift($req->notes, $note);
 | 
			
		||||
        Document::updateFull(Data::REQ_TABLE, $reqId, $req);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a new request
 | 
			
		||||
     * 
 | 
			
		||||
     * @param Request $req The request to be added
 | 
			
		||||
     */
 | 
			
		||||
    public static function addRequest(Request $req)
 | 
			
		||||
    {
 | 
			
		||||
        Document::insert(Data::REQ_TABLE, $req->id, $req);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Map an array of `Request`s to an array of `JournalRequest`s
 | 
			
		||||
     * 
 | 
			
		||||
     * @param Request[] $reqs The requests to map
 | 
			
		||||
     * @param bool $full Whether to include history and notes (true) or not (false)
 | 
			
		||||
     * @return JournalRequest[] The journal request objects
 | 
			
		||||
     */
 | 
			
		||||
    private static function mapToJournalRequest(array $reqs, bool $full): array
 | 
			
		||||
    {
 | 
			
		||||
        return array_map(fn (Request $req) => new JournalRequest($req, $full), $reqs);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get journal requests for the given user by "answered" status
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $userId The ID of the user for whom requests should be retrieved
 | 
			
		||||
     * @param string $op The JSON Path operator to use for comparison (`==` or `<>`)
 | 
			
		||||
     * @return JournalRequest[] The journal request objects
 | 
			
		||||
     */
 | 
			
		||||
    private static function getJournalByAnswered(string $userId, string $op): array
 | 
			
		||||
    {
 | 
			
		||||
        $sql = Query::selectFromTable(Data::REQ_TABLE)
 | 
			
		||||
            . ' WHERE ' . Query::whereDataContains(':criteria') . ' AND ' . Query::whereJsonPathMatches(':path');
 | 
			
		||||
        $params = [
 | 
			
		||||
            ':criteria' => Query::jsonbDocParam([ 'userId' => $userId ]),
 | 
			
		||||
            ':path'     => '$.history[*].action (@ ' . $op . ' "' . RequestAction::Answered->name . '")'
 | 
			
		||||
        ];
 | 
			
		||||
        return Data::mapToJournalRequest(
 | 
			
		||||
            Document::customList($sql, $params, Request::class, Document::mapFromJson(...)), true);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve all answered requests for this user
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $userId The ID of the user for whom answered requests should be retrieved
 | 
			
		||||
     * @return JournalRequest[] The answered requests
 | 
			
		||||
     */
 | 
			
		||||
    public static function getAnsweredRequests(string $userId): array
 | 
			
		||||
    {
 | 
			
		||||
        $answered = Data::getJournalByAnswered($userId, '==');
 | 
			
		||||
        usort($answered,
 | 
			
		||||
            fn (JournalRequest $a, JournalRequest $b) => $a->asOf == $b->asOf ? 0 : ($a->asOf > $b->asOf ? -1 : 1));
 | 
			
		||||
        return $answered;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the user's current prayer request journal
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $userId The ID of the user whose journal should be retrieved
 | 
			
		||||
     * @return JournalRequest[] The journal request objects
 | 
			
		||||
     */
 | 
			
		||||
    public static function getJournal(string $userId): array
 | 
			
		||||
    {
 | 
			
		||||
        $reqs = data::getJournalByAnswered($userId, '<>');
 | 
			
		||||
        usort($reqs,
 | 
			
		||||
            fn (JournalRequest $a, JournalRequest $b) => $a->asOf == $b->asOf ? 0 : ($a->asOf < $b->asOf ? -1 : 1));
 | 
			
		||||
        return $reqs;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								src/app/composer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/app/composer.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
{
 | 
			
		||||
    "require": {
 | 
			
		||||
        "leafs/leaf": "^3.0",
 | 
			
		||||
        "leafs/bareui": "^1.1",
 | 
			
		||||
        "leafs/db": "^2.1",
 | 
			
		||||
        "netresearch/jsonmapper": "^4.2",
 | 
			
		||||
        "visus/cuid2": "^3.0"
 | 
			
		||||
    },
 | 
			
		||||
    "autoload": {
 | 
			
		||||
        "psr-4": {
 | 
			
		||||
            "BitBadger\\PgSQL\\Documents\\": [ "./documents" ],
 | 
			
		||||
            "MyPrayerJournal\\": [ "." ],
 | 
			
		||||
            "MyPrayerJournal\\Domain\\": [ "./domain" ]
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										635
									
								
								src/app/composer.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										635
									
								
								src/app/composer.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,635 @@
 | 
			
		||||
{
 | 
			
		||||
    "_readme": [
 | 
			
		||||
        "This file locks the dependencies of your project to a known state",
 | 
			
		||||
        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
 | 
			
		||||
        "This file is @generated automatically"
 | 
			
		||||
    ],
 | 
			
		||||
    "content-hash": "3e1594cc7c6f8fe6041e1c22822e0ab2",
 | 
			
		||||
    "packages": [
 | 
			
		||||
        {
 | 
			
		||||
            "name": "leafs/anchor",
 | 
			
		||||
            "version": "v1.5.0",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/leafsphp/anchor.git",
 | 
			
		||||
                "reference": "debc228afd63d46d94d0c1d02629c2d912ecb4ee"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/leafsphp/anchor/zipball/debc228afd63d46d94d0c1d02629c2d912ecb4ee",
 | 
			
		||||
                "reference": "debc228afd63d46d94d0c1d02629c2d912ecb4ee",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "pestphp/pest": "^1.21"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Leaf\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Michael Darko",
 | 
			
		||||
                    "email": "mickdd22@gmail.com",
 | 
			
		||||
                    "homepage": "https://mychi.netlify.app",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Leaf PHP util module",
 | 
			
		||||
            "homepage": "https://leafphp.netlify.app/#/",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "framework",
 | 
			
		||||
                "leaf",
 | 
			
		||||
                "php",
 | 
			
		||||
                "util"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/leafsphp/anchor/issues",
 | 
			
		||||
                "source": "https://github.com/leafsphp/anchor/tree/v1.5.0"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": [
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://github.com/leafsphp",
 | 
			
		||||
                    "type": "github"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://opencollective.com/leaf",
 | 
			
		||||
                    "type": "open_collective"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2023-07-09T00:51:55+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "leafs/bareui",
 | 
			
		||||
            "version": "v1.1.1",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/leafsphp/bareui.git",
 | 
			
		||||
                "reference": "a84d855be5ba319a9c0c695d65d25aabf959b23b"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/leafsphp/bareui/zipball/a84d855be5ba319a9c0c695d65d25aabf959b23b",
 | 
			
		||||
                "reference": "a84d855be5ba319a9c0c695d65d25aabf959b23b",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "files": [
 | 
			
		||||
                    "src/scripts.php"
 | 
			
		||||
                ],
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Leaf\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Michael Darko",
 | 
			
		||||
                    "email": "mickdd22@gmail.com",
 | 
			
		||||
                    "homepage": "https://mychi.netlify.app",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Leaf PHP bareui templating engine",
 | 
			
		||||
            "homepage": "https://leafphp.netlify.app/#/",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "framework",
 | 
			
		||||
                "leaf",
 | 
			
		||||
                "php",
 | 
			
		||||
                "simple templating",
 | 
			
		||||
                "template",
 | 
			
		||||
                "view"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/leafsphp/bareui/issues",
 | 
			
		||||
                "source": "https://github.com/leafsphp/bareui/tree/v1.1.1"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": [
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://github.com/leafsphp",
 | 
			
		||||
                    "type": "github"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://opencollective.com/leaf",
 | 
			
		||||
                    "type": "open_collective"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2023-05-17T08:42:17+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "leafs/db",
 | 
			
		||||
            "version": "v2.1.0",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/leafsphp/db.git",
 | 
			
		||||
                "reference": "5b663754b552dcc4dde3314e425afe75084a02f3"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/leafsphp/db/zipball/5b663754b552dcc4dde3314e425afe75084a02f3",
 | 
			
		||||
                "reference": "5b663754b552dcc4dde3314e425afe75084a02f3",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "friendsofphp/php-cs-fixer": "^3.14",
 | 
			
		||||
                "leafs/alchemy": "^1.0",
 | 
			
		||||
                "pestphp/pest": "^1.21"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "files": [
 | 
			
		||||
                    "src/functions.php"
 | 
			
		||||
                ],
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Leaf\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Michael Darko",
 | 
			
		||||
                    "email": "mickdd22@gmail.com",
 | 
			
		||||
                    "homepage": "https://mychi.netlify.app",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Leaf PHP db module.",
 | 
			
		||||
            "homepage": "https://leafphp.netlify.app/#/",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "database",
 | 
			
		||||
                "framework",
 | 
			
		||||
                "leaf",
 | 
			
		||||
                "orm",
 | 
			
		||||
                "php"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/leafsphp/db/issues",
 | 
			
		||||
                "source": "https://github.com/leafsphp/db/tree/v2.1.0"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": [
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://github.com/leafsphp",
 | 
			
		||||
                    "type": "github"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://opencollective.com/leaf",
 | 
			
		||||
                    "type": "open_collective"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2023-02-27T17:16:54+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "leafs/exception",
 | 
			
		||||
            "version": "v3.2.1",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/leafsphp/exceptions.git",
 | 
			
		||||
                "reference": "064a24f34c719a8121da2d737eddc9917ddca263"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/leafsphp/exceptions/zipball/064a24f34c719a8121da2d737eddc9917ddca263",
 | 
			
		||||
                "reference": "064a24f34c719a8121da2d737eddc9917ddca263",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "php": "^5.5.9 || ^7.0 || ^8.0",
 | 
			
		||||
                "psr/log": "^1.0.1 || ^2.0 || ^3.0"
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "mockery/mockery": "^0.9 || ^1.0",
 | 
			
		||||
                "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
 | 
			
		||||
                "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
 | 
			
		||||
            },
 | 
			
		||||
            "suggest": {
 | 
			
		||||
                "symfony/var-dumper": "Pretty print complex values better with var-dumper available",
 | 
			
		||||
                "whoops/soap": "Formats errors as SOAP responses"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Leaf\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Filipe Dobreira",
 | 
			
		||||
                    "homepage": "https://github.com/filp",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Michael Darko",
 | 
			
		||||
                    "email": "mickdd22@gmail.com",
 | 
			
		||||
                    "homepage": "https://mychi.netlify.app",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Error handler for leaf (fork of whoops)",
 | 
			
		||||
            "homepage": "https://github.com/leafsphp/exception",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "error",
 | 
			
		||||
                "exception",
 | 
			
		||||
                "handling",
 | 
			
		||||
                "library",
 | 
			
		||||
                "throwable",
 | 
			
		||||
                "whoops"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "source": "https://github.com/leafsphp/exceptions/tree/v3.2.1"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": [
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://github.com/denis-sokolov",
 | 
			
		||||
                    "type": "github"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2023-07-08T12:03:30+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "leafs/http",
 | 
			
		||||
            "version": "v2.2.3",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/leafsphp/http.git",
 | 
			
		||||
                "reference": "77eebb3db4c722f04f9ca53ee28e9c62a5294505"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/leafsphp/http/zipball/77eebb3db4c722f04f9ca53ee28e9c62a5294505",
 | 
			
		||||
                "reference": "77eebb3db4c722f04f9ca53ee28e9c62a5294505",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "leafs/anchor": "*"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "files": [
 | 
			
		||||
                    "src/functions.php"
 | 
			
		||||
                ],
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Leaf\\Http\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Michael Darko",
 | 
			
		||||
                    "email": "mickdd22@gmail.com",
 | 
			
		||||
                    "homepage": "https://mychi.netlify.app",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Leaf PHP HTTP module.",
 | 
			
		||||
            "homepage": "https://leafphp.dev/modules/http/v/2/request.html",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "framework",
 | 
			
		||||
                "headers",
 | 
			
		||||
                "http",
 | 
			
		||||
                "leaf",
 | 
			
		||||
                "php",
 | 
			
		||||
                "request",
 | 
			
		||||
                "response"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/leafsphp/http/issues",
 | 
			
		||||
                "source": "https://github.com/leafsphp/http/tree/v2.2.3"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": [
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://github.com/leafsphp",
 | 
			
		||||
                    "type": "github"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://opencollective.com/leaf",
 | 
			
		||||
                    "type": "open_collective"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2023-04-20T20:43:16+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "leafs/leaf",
 | 
			
		||||
            "version": "v3.4.1",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/leafsphp/leaf.git",
 | 
			
		||||
                "reference": "8964d19c3c129721d1baa403141499ae59c10c7f"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/leafsphp/leaf/zipball/8964d19c3c129721d1baa403141499ae59c10c7f",
 | 
			
		||||
                "reference": "8964d19c3c129721d1baa403141499ae59c10c7f",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "leafs/anchor": "*",
 | 
			
		||||
                "leafs/exception": "*",
 | 
			
		||||
                "leafs/http": "*",
 | 
			
		||||
                "leafs/router": "*"
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "friendsofphp/php-cs-fixer": "^3.0",
 | 
			
		||||
                "pestphp/pest": "^1.21"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "files": [
 | 
			
		||||
                    "src/functions.php"
 | 
			
		||||
                ],
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Leaf\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Michael Darko",
 | 
			
		||||
                    "email": "mickdd22@gmail.com",
 | 
			
		||||
                    "homepage": "https://mychi.netlify.app",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Simple, performant and powerful PHP micro-framework for rapid web app & API development",
 | 
			
		||||
            "homepage": "https://leafphp.dev",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "framework",
 | 
			
		||||
                "leaf",
 | 
			
		||||
                "microframework",
 | 
			
		||||
                "php",
 | 
			
		||||
                "rest",
 | 
			
		||||
                "router"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/leafsphp/leaf/issues",
 | 
			
		||||
                "source": "https://github.com/leafsphp/leaf/tree/v3.4.1"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": [
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://github.com/leafsphp",
 | 
			
		||||
                    "type": "github"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://opencollective.com/leaf",
 | 
			
		||||
                    "type": "open_collective"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2023-07-08T12:13:03+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "leafs/router",
 | 
			
		||||
            "version": "v0.2.3",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/leafsphp/router.git",
 | 
			
		||||
                "reference": "d7d66f7e76714885d878e70c7cc2c117d5c998ad"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/leafsphp/router/zipball/d7d66f7e76714885d878e70c7cc2c117d5c998ad",
 | 
			
		||||
                "reference": "d7d66f7e76714885d878e70c7cc2c117d5c998ad",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "leafs/anchor": "*",
 | 
			
		||||
                "leafs/http": "*"
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "pestphp/pest": "^1.21"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Leaf\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Michael Darko",
 | 
			
		||||
                    "email": "mickdd22@gmail.com",
 | 
			
		||||
                    "homepage": "https://mychi.netlify.app",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Leaf router module for Leaf PHP.",
 | 
			
		||||
            "homepage": "https://leafphp.netlify.app/#/modules/router",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "framework",
 | 
			
		||||
                "leaf",
 | 
			
		||||
                "php",
 | 
			
		||||
                "rest",
 | 
			
		||||
                "router"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/leafsphp/router/issues",
 | 
			
		||||
                "source": "https://github.com/leafsphp/router/tree/v0.2.3"
 | 
			
		||||
            },
 | 
			
		||||
            "funding": [
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://github.com/leafsphp",
 | 
			
		||||
                    "type": "github"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "url": "https://opencollective.com/leaf",
 | 
			
		||||
                    "type": "open_collective"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "time": "2023-06-30T09:40:09+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "netresearch/jsonmapper",
 | 
			
		||||
            "version": "v4.2.0",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/cweiske/jsonmapper.git",
 | 
			
		||||
                "reference": "f60565f8c0566a31acf06884cdaa591867ecc956"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/f60565f8c0566a31acf06884cdaa591867ecc956",
 | 
			
		||||
                "reference": "f60565f8c0566a31acf06884cdaa591867ecc956",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "ext-json": "*",
 | 
			
		||||
                "ext-pcre": "*",
 | 
			
		||||
                "ext-reflection": "*",
 | 
			
		||||
                "ext-spl": "*",
 | 
			
		||||
                "php": ">=7.1"
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0",
 | 
			
		||||
                "squizlabs/php_codesniffer": "~3.5"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "psr-0": {
 | 
			
		||||
                    "JsonMapper": "src/"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "OSL-3.0"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Christian Weiske",
 | 
			
		||||
                    "email": "cweiske@cweiske.de",
 | 
			
		||||
                    "homepage": "http://github.com/cweiske/jsonmapper/",
 | 
			
		||||
                    "role": "Developer"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Map nested JSON structures onto PHP classes",
 | 
			
		||||
            "support": {
 | 
			
		||||
                "email": "cweiske@cweiske.de",
 | 
			
		||||
                "issues": "https://github.com/cweiske/jsonmapper/issues",
 | 
			
		||||
                "source": "https://github.com/cweiske/jsonmapper/tree/v4.2.0"
 | 
			
		||||
            },
 | 
			
		||||
            "time": "2023-04-09T17:37:40+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "psr/log",
 | 
			
		||||
            "version": "3.0.0",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/php-fig/log.git",
 | 
			
		||||
                "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
 | 
			
		||||
                "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "php": ">=8.0.0"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "extra": {
 | 
			
		||||
                "branch-alias": {
 | 
			
		||||
                    "dev-master": "3.x-dev"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Psr\\Log\\": "src"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "MIT"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "PHP-FIG",
 | 
			
		||||
                    "homepage": "https://www.php-fig.org/"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Common interface for logging libraries",
 | 
			
		||||
            "homepage": "https://github.com/php-fig/log",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "log",
 | 
			
		||||
                "psr",
 | 
			
		||||
                "psr-3"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "source": "https://github.com/php-fig/log/tree/3.0.0"
 | 
			
		||||
            },
 | 
			
		||||
            "time": "2021-07-14T16:46:02+00:00"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "visus/cuid2",
 | 
			
		||||
            "version": "3.0.0",
 | 
			
		||||
            "source": {
 | 
			
		||||
                "type": "git",
 | 
			
		||||
                "url": "https://github.com/visus-io/php-cuid2.git",
 | 
			
		||||
                "reference": "0a422fa4785c3ce1f01f60cec35684ed31f46860"
 | 
			
		||||
            },
 | 
			
		||||
            "dist": {
 | 
			
		||||
                "type": "zip",
 | 
			
		||||
                "url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/0a422fa4785c3ce1f01f60cec35684ed31f46860",
 | 
			
		||||
                "reference": "0a422fa4785c3ce1f01f60cec35684ed31f46860",
 | 
			
		||||
                "shasum": ""
 | 
			
		||||
            },
 | 
			
		||||
            "require": {
 | 
			
		||||
                "php": "^8.1"
 | 
			
		||||
            },
 | 
			
		||||
            "require-dev": {
 | 
			
		||||
                "ergebnis/composer-normalize": "^2.29",
 | 
			
		||||
                "ext-ctype": "*",
 | 
			
		||||
                "php-parallel-lint/php-parallel-lint": "^1.3",
 | 
			
		||||
                "phpstan/phpstan": "^1.9",
 | 
			
		||||
                "phpunit/phpunit": "^10.0",
 | 
			
		||||
                "squizlabs/php_codesniffer": "^3.7",
 | 
			
		||||
                "vimeo/psalm": "^5.4"
 | 
			
		||||
            },
 | 
			
		||||
            "suggest": {
 | 
			
		||||
                "ext-gmp": "*"
 | 
			
		||||
            },
 | 
			
		||||
            "type": "library",
 | 
			
		||||
            "autoload": {
 | 
			
		||||
                "files": [
 | 
			
		||||
                    "src/compat.php"
 | 
			
		||||
                ],
 | 
			
		||||
                "psr-4": {
 | 
			
		||||
                    "Visus\\Cuid2\\": "src/"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "notification-url": "https://packagist.org/downloads/",
 | 
			
		||||
            "license": [
 | 
			
		||||
                "Apache-2.0"
 | 
			
		||||
            ],
 | 
			
		||||
            "authors": [
 | 
			
		||||
                {
 | 
			
		||||
                    "name": "Alan Brault",
 | 
			
		||||
                    "email": "alan.brault@visus.io"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "A PHP library for generating collision-resistant ids (CUIDs).",
 | 
			
		||||
            "keywords": [
 | 
			
		||||
                "cuid",
 | 
			
		||||
                "identifier"
 | 
			
		||||
            ],
 | 
			
		||||
            "support": {
 | 
			
		||||
                "issues": "https://github.com/visus-io/php-cuid2/issues",
 | 
			
		||||
                "source": "https://github.com/visus-io/php-cuid2/tree/3.0.0"
 | 
			
		||||
            },
 | 
			
		||||
            "time": "2023-08-11T16:22:53+00:00"
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "packages-dev": [],
 | 
			
		||||
    "aliases": [],
 | 
			
		||||
    "minimum-stability": "stable",
 | 
			
		||||
    "stability-flags": [],
 | 
			
		||||
    "prefer-stable": false,
 | 
			
		||||
    "prefer-lowest": false,
 | 
			
		||||
    "platform": [],
 | 
			
		||||
    "platform-dev": [],
 | 
			
		||||
    "plugin-api-version": "2.3.0"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								src/app/documents/Configuration.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/app/documents/Configuration.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace BitBadger\PgSQL\Documents;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Document table configuration
 | 
			
		||||
 */
 | 
			
		||||
class Configuration
 | 
			
		||||
{
 | 
			
		||||
    /** @var string $connectionString The connection string to use when establishing a database connection */
 | 
			
		||||
    public static string $connectionString = "";
 | 
			
		||||
 | 
			
		||||
    /** @var ?\PDO $conn The active connection */
 | 
			
		||||
    private static ?\PDO $conn = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the database connection, connecting on first request
 | 
			
		||||
     * 
 | 
			
		||||
     * @return PDO The PDO object representing the connection
 | 
			
		||||
     */
 | 
			
		||||
    public static function getConn(): \PDO
 | 
			
		||||
    {
 | 
			
		||||
        if (is_null(Configuration::$conn)) {
 | 
			
		||||
            Configuration::$conn = new \PDO(Configuration::$connectionString);
 | 
			
		||||
        }
 | 
			
		||||
        return Configuration::$conn;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
require('functions.php');
 | 
			
		||||
							
								
								
									
										57
									
								
								src/app/documents/Definition.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/app/documents/Definition.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace BitBadger\PgSQL\Documents;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Methods to define tables and indexes for document tables
 | 
			
		||||
 */
 | 
			
		||||
class Definition
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a statement to create a document table
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $name The name of the table to create
 | 
			
		||||
     * @return string A `CREATE TABLE` statement for the document table
 | 
			
		||||
     */
 | 
			
		||||
    public static function createTable(string $name): string
 | 
			
		||||
    {
 | 
			
		||||
        return "CREATE TABLE IF NOT EXISTS $name (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a statement to create an index on a document table
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $name The name of the table for which the index should be created
 | 
			
		||||
     * @param DocumentIndex $type The type of index to create
 | 
			
		||||
     * @return string A `CREATE INDEX` statement for the given table
 | 
			
		||||
     */
 | 
			
		||||
    public static function createIndex(string $name, DocumentIndex $type): string
 | 
			
		||||
    {
 | 
			
		||||
        $extraOps       = $type == DocumentIndex::Full ? "" : " jsonb_path_ops";
 | 
			
		||||
        $schemaAndTable = explode(".", $name);
 | 
			
		||||
        $tableName      = end($schemaAndTable);
 | 
			
		||||
        return "CREATE INDEX IF NOT EXISTS idx_$tableName ON $name USING GIN (data$extraOps)";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Ensure the given document table exists
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $name The name of the table
 | 
			
		||||
     */
 | 
			
		||||
    public static function ensureTable(string $name)
 | 
			
		||||
    {
 | 
			
		||||
        pdo()->query(Definition::createTable($name))->execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Ensure an index on the given document table exists
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $name The name of the table for which the index should be created
 | 
			
		||||
     * @param DocumentIndex $type The type of index to create
 | 
			
		||||
     */
 | 
			
		||||
    public static function ensureIndex(string $name, DocumentIndex $type)
 | 
			
		||||
    {
 | 
			
		||||
        pdo()->query(Definition::createIndex($name, $type))->execute();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										454
									
								
								src/app/documents/Document.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										454
									
								
								src/app/documents/Document.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,454 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace BitBadger\PgSQL\Documents;
 | 
			
		||||
 | 
			
		||||
use PDOStatement;
 | 
			
		||||
 | 
			
		||||
/** Document manipulation functions */
 | 
			
		||||
class Document
 | 
			
		||||
{
 | 
			
		||||
    /** JSON Mapper instance to use for creating a domain type instance from a document */
 | 
			
		||||
    private static ?\JsonMapper $mapper = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Map a domain type from the JSON document retrieved
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $columnName The name of the column from the database
 | 
			
		||||
     * @param array $result An associative array with a single result to be mapped
 | 
			
		||||
     * @param class-string<Type> $className The name of the class onto which the JSON will be mapped
 | 
			
		||||
     * @return Type The domain type
 | 
			
		||||
     */
 | 
			
		||||
    public static function mapDocFromJson(string $columnName, array $result, string $className): mixed
 | 
			
		||||
    {
 | 
			
		||||
        if (is_null(Document::$mapper)) {
 | 
			
		||||
            Document::$mapper = new \JsonMapper();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $mapped = new $className();
 | 
			
		||||
        Document::$mapper->map(json_decode($result[$columnName]), $mapped);
 | 
			
		||||
        return $mapped;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Map a domain type from the JSON document retrieved
 | 
			
		||||
     * 
 | 
			
		||||
     * @param array $result An associative array with a single result to be mapped
 | 
			
		||||
     * @param class-string<Type> $className The name of the class onto which the JSON will be mapped
 | 
			
		||||
     * @return Type The domain type
 | 
			
		||||
     */
 | 
			
		||||
    public static function mapFromJson(array $result, string $className): mixed
 | 
			
		||||
    {
 | 
			
		||||
        return Document::mapDocFromJson('data', $result, $className);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute a document-focused statement that does not return results
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $query The query to be executed
 | 
			
		||||
     * @param string $docId The ID of the document on which action should be taken
 | 
			
		||||
     * @param array|object $document The array or object representing the document
 | 
			
		||||
     */
 | 
			
		||||
    private static function executeNonQuery(string $query, string $docId, array|object $document)
 | 
			
		||||
    {
 | 
			
		||||
        $nonQuery = pdo()->prepare($query);
 | 
			
		||||
        $nonQuery->bindParam('@id', $docId);
 | 
			
		||||
        $nonQuery->bindParam('@data', Query::jsonbDocParam($document));
 | 
			
		||||
        $nonQuery->execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Insert a document
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table into which a document should be inserted
 | 
			
		||||
     * @param string $docId The ID of the document to be inserted
 | 
			
		||||
     * @param array|object $document The array or object representing the document
 | 
			
		||||
     */
 | 
			
		||||
    public static function insert(string $tableName, string $docId, array|object $document)
 | 
			
		||||
    {
 | 
			
		||||
        Document::executeNonQuery(Query::insert($tableName), $docId, $document);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save (upsert) a document
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table into which a document should be inserted
 | 
			
		||||
     * @param string $docId The ID of the document to be inserted
 | 
			
		||||
     * @param array|object $document The array or object representing the document
 | 
			
		||||
     */
 | 
			
		||||
    public static function save(string $tableName, string $docId, array|object $document)
 | 
			
		||||
    {
 | 
			
		||||
        Document::executeNonQuery(Query::save($tableName), $docId, $document);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Count all documents in a table
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which documents should be counted
 | 
			
		||||
     * @return int The number of documents in the table
 | 
			
		||||
     */
 | 
			
		||||
    public static function countAll(string $tableName): int
 | 
			
		||||
    {
 | 
			
		||||
        $result = pdo()->query(Query::countAll($tableName))->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return intval($result['it']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Count documents in a table by JSON containment `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which documents should be counted
 | 
			
		||||
     * @param array|object $criteria The criteria for the JSON containment query
 | 
			
		||||
     * @return int The number of documents in the table matching the JSON containment query
 | 
			
		||||
     */
 | 
			
		||||
    public static function countByContains(string $tableName, array|object $criteria): int
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::countByContains($tableName));
 | 
			
		||||
        $query->bindParam('@criteria', Query::jsonbDocParam($criteria));
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        $result = $query->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return intval($result['it']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Count documents in a table by JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which documents should be counted
 | 
			
		||||
     * @param string $jsonPath The JSON Path to be matched
 | 
			
		||||
     * @return int The number of documents in the table matching the JSON Path
 | 
			
		||||
     */
 | 
			
		||||
    public static function countByJsonPath(string $tableName, string $jsonPath): int
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::countByContains($tableName));
 | 
			
		||||
        $query->bindParam('@path', $jsonPath);
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        $result = $query->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return intval($result['it']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine if a document exists for the given ID
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which existence should be checked
 | 
			
		||||
     * @param string $docId The ID of the document whose existence should be checked
 | 
			
		||||
     * @return bool True if the document exists, false if not
 | 
			
		||||
     */
 | 
			
		||||
    public static function existsById(string $tableName, string $docId): bool
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::existsById($tableName));
 | 
			
		||||
        $query->bindParam('@id', $docId);
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        $result = $query->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return boolval($result['it']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine if documents exist by JSON containment `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which existence should be checked
 | 
			
		||||
     * @param array|object $criteria The criteria for the JSON containment query
 | 
			
		||||
     * @return int True if any documents in the table match the JSON containment query, false if not
 | 
			
		||||
     */
 | 
			
		||||
    public static function existsByContains(string $tableName, array|object $criteria): bool
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::existsByContains($tableName));
 | 
			
		||||
        $query->bindParam('@criteria', Query::jsonbDocParam($criteria));
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        $result = $query->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return boolval($result['it']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine if documents exist by JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which existence should be checked
 | 
			
		||||
     * @param string $jsonPath The JSON Path to be matched
 | 
			
		||||
     * @return int True if any documents in the table match the JSON Path, false if not
 | 
			
		||||
     */
 | 
			
		||||
    public static function existsByJsonPath(string $tableName, string $jsonPath): bool
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::existsByJsonPath($tableName));
 | 
			
		||||
        $query->bindParam('@path', $jsonPath);
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        $result = $query->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return boolval($result['it']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Map the results of a query to domain type objects
 | 
			
		||||
     * 
 | 
			
		||||
     * @param \PDOStatement $stmt The statement with the query to be run
 | 
			
		||||
     * @param class-string<Type> $className The type of document to be mapped
 | 
			
		||||
     * @return array<Type> The documents matching the query
 | 
			
		||||
     */
 | 
			
		||||
    private static function mapResults(\PDOStatement $stmt, string $className): array
 | 
			
		||||
    {
 | 
			
		||||
        return array_map(fn ($it) => Document::mapFromJson($it, $className), $stmt->fetchAll(\PDO::FETCH_ASSOC));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve all documents in a table
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which all documents should be retrieved
 | 
			
		||||
     * @param class-string<Type> $className The type of document to be retrieved
 | 
			
		||||
     * @return array<Type> An array of documents
 | 
			
		||||
     */
 | 
			
		||||
    public static function findAll(string $tableName, string $className): array
 | 
			
		||||
    {
 | 
			
		||||
        return Document::mapResults(pdo()->query(Query::selectFromTable($tableName)), $className);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve a document by its ID
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which a document should be retrieved
 | 
			
		||||
     * @param string $docId The ID of the document to retrieve
 | 
			
		||||
     * @param class-string<Type> $className The type of document to retrieve
 | 
			
		||||
     * @return Type|null The document, or null if it is not found
 | 
			
		||||
     */
 | 
			
		||||
    public static function findById(string $tableName, string $docId, string $className): mixed
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::findById($tableName));
 | 
			
		||||
        $query->bindParam(':id', $docId);
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        $result = $query->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return $result ? Document::mapFromJson($result, $className) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a JSON containment query
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which documents should be retrieved
 | 
			
		||||
     * @param array|object $criteria The criteria for the JSON containment query
 | 
			
		||||
     * @return \PDOStatement An executed query ready to be fetched
 | 
			
		||||
     */
 | 
			
		||||
    private static function queryByContains(string $tableName, array|object $criteria): \PDOStatement
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::findByContains($tableName));
 | 
			
		||||
        $query->bindParam('@criteria', Query::jsonbDocParam($criteria));
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        return $query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve documents in a table via JSON containment `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which documents should be retrieved
 | 
			
		||||
     * @param array|object $criteria The criteria for the JSON containment query
 | 
			
		||||
     * @param class-string<Type> $className The type of document to be retrieved
 | 
			
		||||
     * @return array<Type> Documents matching the JSON containment query
 | 
			
		||||
     */
 | 
			
		||||
    public static function findByContains(string $tableName, array|object $criteria, string $className): array
 | 
			
		||||
    {
 | 
			
		||||
        return Document::mapResults(Document::queryByContains($tableName, $criteria), $className);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the first matching document via JSON containment `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which documents should be retrieved
 | 
			
		||||
     * @param array|object $criteria The criteria for the JSON containment query
 | 
			
		||||
     * @param class-string<Type> $className The type of document to be retrieved
 | 
			
		||||
     * @return Type|null The document, or null if none match
 | 
			
		||||
     */
 | 
			
		||||
    public static function findFirstByContains(string $tableName, array|object $criteria, string $className): mixed
 | 
			
		||||
    {
 | 
			
		||||
        $query = Document::queryByContains($tableName, $criteria);
 | 
			
		||||
        $result = $query->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return $result ? Document::mapFromJson($result, $className) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve documents in a table via JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which documents should be retrieved
 | 
			
		||||
     * @param string $jsonPath The JSON Path to be matched
 | 
			
		||||
     * @return \PDOStatement An executed query ready to be fetched
 | 
			
		||||
     */
 | 
			
		||||
    private static function queryByJsonPath(string $tableName, string $jsonPath): \PDOStatement
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::findByJsonPath($tableName));
 | 
			
		||||
        $query->bindParam('@path', $jsonPath);
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        return $query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve documents in a table via JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which documents should be retrieved
 | 
			
		||||
     * @param string $jsonPath The JSON Path to be matched
 | 
			
		||||
     * @param class-string<Type> $className The type of document to be retrieved
 | 
			
		||||
     * @return array<Type> Documents matching the JSON Path
 | 
			
		||||
     */
 | 
			
		||||
    public static function findByJsonPath(string $tableName, string $jsonPath, string $className): array
 | 
			
		||||
    {
 | 
			
		||||
        return Document::mapResults(Document::queryByJsonPath($tableName, $jsonPath), $className);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the first matching document via JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which documents should be retrieved
 | 
			
		||||
     * @param string $jsonPath The JSON Path to be matched
 | 
			
		||||
     * @param class-string<Type> $className The type of document to be retrieved
 | 
			
		||||
     * @return Type|null The document, or null if none match
 | 
			
		||||
     */
 | 
			
		||||
    public static function findFirstByJsonPath(string $tableName, string $jsonPath, string $className): mixed
 | 
			
		||||
    {
 | 
			
		||||
        $query = Document::queryByJsonPath($tableName, $jsonPath);
 | 
			
		||||
        $result = $query->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return $result ? Document::mapFromJson($result, $className) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update a full document
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table in which the document should be updated
 | 
			
		||||
     * @param string $docId The ID of the document to be updated
 | 
			
		||||
     * @param array|object $document The document to be updated
 | 
			
		||||
     */
 | 
			
		||||
    public static function updateFull(string $tableName, string $docId, array|object $document)
 | 
			
		||||
    {
 | 
			
		||||
        Document::executeNonQuery(Query::updateFull($tableName), $docId, $document);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update a partial document by its ID
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table in which the document should be updated
 | 
			
		||||
     * @param string $docId The ID of the document to be updated
 | 
			
		||||
     * @param array|object $document The partial document to be updated
 | 
			
		||||
     */
 | 
			
		||||
    public static function updatePartialById(string $tableName, string $docId, array|object $document)
 | 
			
		||||
    {
 | 
			
		||||
        Document::executeNonQuery(Query::updatePartialById($tableName), $docId, $document);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update partial documents by JSON containment `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table in which documents should be updated
 | 
			
		||||
     * @param array|object $criteria The JSON containment criteria
 | 
			
		||||
     * @param array|object $document The document to be updated
 | 
			
		||||
     */
 | 
			
		||||
    public static function updatePartialByContains(string $tableName, array|object $criteria, array|object $document)
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::updatePartialByContains($tableName));
 | 
			
		||||
        $query->bindParam('@data', Query::jsonbDocParam($document));
 | 
			
		||||
        $query->bindParam('@criteria', Query::jsonbDocParam($criteria));
 | 
			
		||||
        $query->execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update partial documents by JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table in which documents should be updated
 | 
			
		||||
     * @param string $jsonPath The JSON Path to be matched
 | 
			
		||||
     * @param array|object $document The document to be updated
 | 
			
		||||
     */
 | 
			
		||||
    public static function updatePartialByJsonPath(string $tableName, string $jsonPath, array|object $document)
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::updatePartialByContains($tableName));
 | 
			
		||||
        $query->bindParam('@data', Query::jsonbDocParam($document));
 | 
			
		||||
        $query->bindParam('@path', $jsonPath);
 | 
			
		||||
        $query->execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a document by its ID
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which a document should be deleted
 | 
			
		||||
     * @param string $docId The ID of the document to be deleted
 | 
			
		||||
     */
 | 
			
		||||
    public static function deleteById(string $tableName, string $docId)
 | 
			
		||||
    {
 | 
			
		||||
        Document::executeNonQuery(Query::deleteById($tableName), $docId, []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete documents by JSON containment `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which documents should be deleted
 | 
			
		||||
     * @param array|object $criteria The criteria for the JSON containment query
 | 
			
		||||
     */
 | 
			
		||||
    public static function deleteByContains(string $tableName, array|object $criteria)
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::deleteByContains($tableName));
 | 
			
		||||
        $query->bindParam('@criteria', Query::jsonbDocParam($criteria));
 | 
			
		||||
        $query->execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete documents by JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The table from which documents should be deleted
 | 
			
		||||
     * @param string $jsonPath The JSON Path expression to be matched
 | 
			
		||||
     */
 | 
			
		||||
    public static function deleteByJsonPath(string $tableName, string $jsonPath)
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare(Query::deleteByJsonPath($tableName));
 | 
			
		||||
        $query->bindParam('@path', $jsonPath);
 | 
			
		||||
        $query->execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: custom
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create and execute a custom query
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $sql The SQL query to execute
 | 
			
		||||
     * @param array $params An associative array of parameters for the SQL query
 | 
			
		||||
     * @return PDOStatement The query, executed and ready to be fetched
 | 
			
		||||
     */
 | 
			
		||||
    private static function createCustomQuery(string $sql, array $params): PDOStatement
 | 
			
		||||
    {
 | 
			
		||||
        $query = pdo()->prepare($sql);
 | 
			
		||||
        foreach ($params as $name => $value) {
 | 
			
		||||
            $query->bindParam($name, $value);
 | 
			
		||||
        }
 | 
			
		||||
        $query->execute();
 | 
			
		||||
        return $query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve documents via a custom query and mapping
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $sql The SQL query to execute
 | 
			
		||||
     * @param array $params An associative array of parameters for the SQL query
 | 
			
		||||
     * @param callable $mapFunc A function that expects an associative array and returns a value of the desired type
 | 
			
		||||
     * @param class-string<Type> $className The type of document to be mapped
 | 
			
		||||
     * @return array<Type> The documents matching the query
 | 
			
		||||
     */
 | 
			
		||||
    public static function customList(string $sql, array $params, string $className, callable $mapFunc): array
 | 
			
		||||
    {
 | 
			
		||||
        return array_map(
 | 
			
		||||
            fn ($it) => $mapFunc($it, $className),
 | 
			
		||||
            Document::createCustomQuery($sql, $params)->fetchAll(\PDO::FETCH_ASSOC));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve a document via a custom query and mapping
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $sql The SQL query to execute
 | 
			
		||||
     * @param array $params An associative array of parameters for the SQL query
 | 
			
		||||
     * @param callable $mapFunc A function that expects an associative array and returns a value of the desired type
 | 
			
		||||
     * @param class-string<Type> $className The type of document to be mapped
 | 
			
		||||
     * @return ?Type The document matching the query, or null if none is found
 | 
			
		||||
     */
 | 
			
		||||
    public static function customSingle(string $sql, array $params, string $className, callable $mapFunc): mixed
 | 
			
		||||
    {
 | 
			
		||||
        $result = Document::createCustomQuery($sql, $params)->fetch(\PDO::FETCH_ASSOC);
 | 
			
		||||
        return $result ? $mapFunc($result, $className) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute a custom query that does not return a result
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $sql The SQL query to execute
 | 
			
		||||
     * @param array $params An associative array of parameters for the SQL query
 | 
			
		||||
     */
 | 
			
		||||
    public static function customNonQuery(string $sql, array $params)
 | 
			
		||||
    {
 | 
			
		||||
        Document::createCustomQuery($sql, $params);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								src/app/documents/DocumentIndex.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/app/documents/DocumentIndex.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace BitBadger\PgSQL\Documents;
 | 
			
		||||
 | 
			
		||||
/** The type of index to generate for the document */
 | 
			
		||||
enum DocumentIndex
 | 
			
		||||
{
 | 
			
		||||
    /** A GIN index with standard operations (all operators supported) */
 | 
			
		||||
    case Full;
 | 
			
		||||
 | 
			
		||||
    /** A GIN index with JSONPath operations (optimized for `@>`, `@?`, `@@` operators) */
 | 
			
		||||
    case Optimized;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										255
									
								
								src/app/documents/Query.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								src/app/documents/Query.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,255 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace BitBadger\PgSQL\Documents;
 | 
			
		||||
 | 
			
		||||
/** Query construction functions */
 | 
			
		||||
class Query
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a `SELECT` clause to retrieve the document data from the given table
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which documents should be selected
 | 
			
		||||
     * @return string A `SELECT` clause for the given table
 | 
			
		||||
     */
 | 
			
		||||
    public static function selectFromTable(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "SELECT data FROM $tableName";
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a `WHERE` clause fragment to implement a @> (JSON contains) condition
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $paramName The name of the parameter for the contains clause
 | 
			
		||||
     * @return string A `WHERE` clause fragment with the named parameter
 | 
			
		||||
     */
 | 
			
		||||
    public static function whereDataContains(string $paramName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "data @> $paramName";
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a `WHERE` clause fragment to implement a @? (JSON Path match) condition
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $paramName THe name of the parameter for the JSON Path match
 | 
			
		||||
     * @return string A `WHERE` clause fragment with the named parameter
 | 
			
		||||
     */
 | 
			
		||||
    public static function whereJsonPathMatches(string $paramName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "data @? {$paramName}::jsonpath";
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a JSONB document parameter
 | 
			
		||||
     * 
 | 
			
		||||
     * @param array|object $it The array or object to become a JSONB parameter
 | 
			
		||||
     * @return string The encoded JSON
 | 
			
		||||
     */
 | 
			
		||||
    public static function jsonbDocParam(array|object $it): string
 | 
			
		||||
    {
 | 
			
		||||
        return json_encode($it);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Create ID and data parameters for a query
 | 
			
		||||
    /* let docParameters<'T> docId (doc : 'T) =
 | 
			
		||||
        [ "@id", Sql.string docId; "@data", jsonbDocParam doc ]
 | 
			
		||||
    */
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to insert a document
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table into which the document will be inserted
 | 
			
		||||
     * @return string The `INSERT` statement (with `@id` and `@data` parameters defined)
 | 
			
		||||
     */
 | 
			
		||||
    public static function insert(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "INSERT INTO $tableName (id, data) VALUES (@id, @data)";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table into which the document will be saved
 | 
			
		||||
     * @return string The `INSERT`/`ON CONFLICT DO UPDATE` statement (with `@id` and `@data` parameters defined)
 | 
			
		||||
     */
 | 
			
		||||
    public static function save(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "INSERT INTO $tableName (id, data) VALUES (@id, @data)
 | 
			
		||||
                  ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data";
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to count all documents in a table
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table whose rows will be counted
 | 
			
		||||
     * @return string A `SELECT` statement to obtain the count of all documents in the given table
 | 
			
		||||
     */
 | 
			
		||||
    public static function countAll(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "SELECT COUNT(id) AS it FROM $tableName";
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to count matching documents using a JSON containment query `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which the count should be obtained
 | 
			
		||||
     * @return string A `SELECT` statement to obtain the count of documents via JSON containment
 | 
			
		||||
     */
 | 
			
		||||
    public static function countByContains(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "SELECT COUNT(id) AS it FROM $tableName WHERE " . Query::whereDataContains('@criteria');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to count matching documents using a JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which the count should be obtained
 | 
			
		||||
     * @return string A `SELECT` statement to obtain the count of documents via JSON Path match
 | 
			
		||||
     */
 | 
			
		||||
    public static function countByJsonPath(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "SELECT COUNT(id) AS it FROM $tableName WHERE " . Query::whereJsonPathMatches('@path');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to determine if a document exists for the given ID
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which existence should be checked
 | 
			
		||||
     * @return string A `SELECT` statement to check existence of a document by its ID
 | 
			
		||||
     */
 | 
			
		||||
    public static function existsById(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "SELECT EXISTS (SELECT 1 FROM $tableName WHERE id = @id) AS it";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to determine if documents exist using a JSON containment query `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which existence should be checked
 | 
			
		||||
     * @return string A `SELECT` statement to check existence of a document by JSON containment
 | 
			
		||||
     */
 | 
			
		||||
    public static function existsByContains(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "SELECT EXISTS (SELECT 1 FROM $tableName WHERE " . Query::whereDataContains('@criteria') . ' AS it';
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to determine if documents exist using a JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which existence should be checked
 | 
			
		||||
     * @return string A `SELECT` statement to check existence of a document by JSON Path match
 | 
			
		||||
     */
 | 
			
		||||
    public static function existsByJsonPath(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "SELECT EXISTS (SELECT 1 FROM $tableName WHERE " . Query::whereJsonPathMatches('@path') . ' AS it';
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to retrieve a document by its ID
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which a document should be retrieved
 | 
			
		||||
     * @return string A `SELECT` statement to retrieve a document by its ID
 | 
			
		||||
     */
 | 
			
		||||
    public static function findById(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return Query::selectFromTable($tableName) . ' WHERE id = :id';
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to retrieve documents using a JSON containment query `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which a document should be retrieved
 | 
			
		||||
     * @return string A `SELECT` statement to retrieve documents by JSON containment
 | 
			
		||||
     */
 | 
			
		||||
    public static function findByContains(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return Query::selectFromTable($tableName) . ' WHERE ' . Query::whereDataContains('@criteria');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to retrieve documents using a JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which a document should be retrieved
 | 
			
		||||
     * @return string A `SELECT` statement to retrieve a documents by JSON Path match
 | 
			
		||||
     */
 | 
			
		||||
    public static function findByJsonPath(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return Query::selectFromTable($tableName) . ' WHERE ' . Query::whereJsonPathMatches('@path');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to update a document, replacing the existing document
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which a document should be updated
 | 
			
		||||
     * @return string An `UPDATE` statement to update a document by its ID
 | 
			
		||||
     */
 | 
			
		||||
    public static function updateFull(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "UPDATE $tableName SET data = @data WHERE id = @id";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to update a document, merging the existing document with the one provided
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which a document should be updated
 | 
			
		||||
     * @return string An `UPDATE` statement to update a document by its ID
 | 
			
		||||
     */
 | 
			
		||||
    public static function updatePartialById(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "UPDATE $tableName SET data = data || @data WHERE id = @id";
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to update partial documents matching a JSON containment query `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which documents should be updated
 | 
			
		||||
     * @return string An `UPDATE` statement to update documents by JSON containment
 | 
			
		||||
     */
 | 
			
		||||
    public static function updatePartialByContains(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "UPDATE $tableName SET data = data || @data WHERE " . Query::whereDataContains('@criteria');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to update partial documents matching a JSON containment query `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table in which documents should be updated
 | 
			
		||||
     * @return string An `UPDATE` statement to update  documents by JSON Path match
 | 
			
		||||
     */
 | 
			
		||||
    public static function updatePartialByJsonPath(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "UPDATE $tableName SET data = data || @data WHERE " . Query::whereJsonPathMatches('@path');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to delete a document by its ID
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which a document should be deleted
 | 
			
		||||
     * @return string A `DELETE` statement to delete a document by its ID
 | 
			
		||||
     */
 | 
			
		||||
    public static function deleteById(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "DELETE FROM $tableName WHERE id = @id";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to delete documents using a JSON containment query `@>`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which documents should be deleted
 | 
			
		||||
     * @return string A `DELETE` statement to delete documents by JSON containment
 | 
			
		||||
     */
 | 
			
		||||
    public static function deleteByContains(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "DELETE FROM $tableName WHERE " . Query::whereDataContains('@criteria');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query to delete documents using a JSON Path match `@?`
 | 
			
		||||
     * 
 | 
			
		||||
     * @param string $tableName The name of the table from which documents should be deleted
 | 
			
		||||
     * @return string A `DELETE` statement to delete documents by JSON Path match
 | 
			
		||||
     */
 | 
			
		||||
    public static function deleteByJsonPath(string $tableName): string
 | 
			
		||||
    {
 | 
			
		||||
        return "DELETE FROM $tableName WHERE " . Query::whereJsonPathMatches('@path');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/app/documents/functions.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/app/documents/functions.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
use BitBadger\PgSQL\Documents\Configuration;
 | 
			
		||||
 | 
			
		||||
if (!function_exists('pdo')) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the active PostgreSQL PDO object
 | 
			
		||||
     *
 | 
			
		||||
     * @return \PDO The data connection from the configuration
 | 
			
		||||
     */
 | 
			
		||||
    function pdo()
 | 
			
		||||
    {
 | 
			
		||||
        return Configuration::getConn();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/app/domain/History.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/app/domain/History.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace MyPrayerJournal\Domain;
 | 
			
		||||
 | 
			
		||||
use DateTimeImmutable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A record of action taken on a prayer request, including updates to its text
 | 
			
		||||
 */
 | 
			
		||||
class History
 | 
			
		||||
{
 | 
			
		||||
    /** The date/time this action was taken */
 | 
			
		||||
    public DateTimeImmutable $asOf;
 | 
			
		||||
 | 
			
		||||
    /** The action taken that generated this history entry */
 | 
			
		||||
    public RequestAction $action = RequestAction::Created;
 | 
			
		||||
 | 
			
		||||
    /** The text of the update, if applicable */
 | 
			
		||||
    public ?string $text = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->asOf = new DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isCreated(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->action == RequestAction::Created;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isPrayed(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->action == RequestAction::Prayed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isAnswered(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->action == RequestAction::Answered;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								src/app/domain/JournalRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/app/domain/JournalRequest.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace MyPrayerJournal\Domain;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A prayer request, along with calculated fields, for use in displaying journal lists
 | 
			
		||||
 */
 | 
			
		||||
class JournalRequest
 | 
			
		||||
{
 | 
			
		||||
    /** The ID of the prayer request */
 | 
			
		||||
    public string $id = '';
 | 
			
		||||
 | 
			
		||||
    /** The ID of the user to whom the prayer request belongs */
 | 
			
		||||
    public string $userId = '';
 | 
			
		||||
 | 
			
		||||
    /** The current text of the request */
 | 
			
		||||
    public string $text = '';
 | 
			
		||||
 | 
			
		||||
    /** The date/time this request was last updated */
 | 
			
		||||
    public \DateTimeImmutable $asOf;
 | 
			
		||||
 | 
			
		||||
    /** The date/time this request was last marked as prayed */
 | 
			
		||||
    public \DateTimeImmutable $lastPrayed;
 | 
			
		||||
 | 
			
		||||
    /** The last action taken on this request */
 | 
			
		||||
    public RequestAction $lastAction = RequestAction::Created;
 | 
			
		||||
 | 
			
		||||
    /** When this request will be shown again after having been snoozed */
 | 
			
		||||
    public ?\DateTimeImmutable $snoozedUntil = null;
 | 
			
		||||
 | 
			
		||||
    /** When this request will be show agains after a non-immediate recurrence */
 | 
			
		||||
    public ?\DateTimeImmutable $showAfter = null;
 | 
			
		||||
 | 
			
		||||
    /** The type of recurrence for this request */
 | 
			
		||||
    public RecurrenceType $recurrenceType = RecurrenceType::Immediate;
 | 
			
		||||
 | 
			
		||||
    /** The units for non-immediate recurrence */
 | 
			
		||||
    public ?int $recurrence = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The history for this request
 | 
			
		||||
     * @var History[] $history
 | 
			
		||||
     */
 | 
			
		||||
    public array $history = [];
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * The notes for this request
 | 
			
		||||
     * @var Note[] $notes
 | 
			
		||||
     */
 | 
			
		||||
    public array $notes = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Constructor
 | 
			
		||||
     * 
 | 
			
		||||
     * @param ?Request $req The request off which this journal request should be populated
 | 
			
		||||
     * @param bool $full Whether to include history and notes (true) or exclude them (false)
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(?Request $req = null, bool $full = false)
 | 
			
		||||
    {
 | 
			
		||||
        if (is_null($req)) {
 | 
			
		||||
            $this->asOf       = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
 | 
			
		||||
            $this->lastPrayed = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->id             = $req->id;
 | 
			
		||||
            $this->userId         = $req->userId;
 | 
			
		||||
            $this->snoozedUntil   = $req->snoozedUntil;
 | 
			
		||||
            $this->showAfter      = $req->showAfter;
 | 
			
		||||
            $this->recurrenceType = $req->recurrenceType;
 | 
			
		||||
            $this->recurrence     = $req->recurrence;
 | 
			
		||||
 | 
			
		||||
            usort($req->history,
 | 
			
		||||
                fn (History $a, History $b) => $a->asOf == $b->asOf ? 0 : ($a->asOf > $b->asOf ? -1 : 1));
 | 
			
		||||
            $this->asOf = $req->history[0]->asOf;
 | 
			
		||||
            $this->lastPrayed =
 | 
			
		||||
                array_values(array_filter($req->history, fn (History $it) => $it->action == RequestAction::Prayed))[0]
 | 
			
		||||
                ?->asOf;
 | 
			
		||||
            
 | 
			
		||||
            if ($full) {
 | 
			
		||||
                usort($req->notes,
 | 
			
		||||
                    fn (Note $a, Note $b) => $a->asOf == $b->asOf ? 0 : ($a->asOf > $b->asOf ? -1 : 1));
 | 
			
		||||
                $this->history = $req->history;
 | 
			
		||||
                $this->notes = $req->notes;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/app/domain/Note.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/app/domain/Note.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace MyPrayerJournal\Domain;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A note entered on a prayer request
 | 
			
		||||
 */
 | 
			
		||||
class Note
 | 
			
		||||
{
 | 
			
		||||
    /** The date/time this note was entered */
 | 
			
		||||
    public \DateTimeImmutable $asOf;
 | 
			
		||||
 | 
			
		||||
    /** The note */
 | 
			
		||||
    public string $notes = '';
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->asOf = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								src/app/domain/RecurrenceType.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/app/domain/RecurrenceType.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace MyPrayerJournal\Domain;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The unit to use when determining when to show a recurring request
 | 
			
		||||
 */
 | 
			
		||||
enum RecurrenceType implements \JsonSerializable
 | 
			
		||||
{
 | 
			
		||||
    /** The request should reappear immediately */
 | 
			
		||||
    case Immediate;
 | 
			
		||||
 | 
			
		||||
    /** The request should reappear after the specified number of hours */
 | 
			
		||||
    case Hours;
 | 
			
		||||
 | 
			
		||||
    /** The request should reappear after the specified number of days */
 | 
			
		||||
    case Days;
 | 
			
		||||
 | 
			
		||||
    /** The request should reappear after the specified number of weeks */
 | 
			
		||||
    case Weeks;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Serialize this enum using its name
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonSerialize(): mixed
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								src/app/domain/Request.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/app/domain/Request.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace MyPrayerJournal\Domain;
 | 
			
		||||
 | 
			
		||||
use Visus\Cuid2\Cuid2;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A prayer request
 | 
			
		||||
 */
 | 
			
		||||
class Request
 | 
			
		||||
{
 | 
			
		||||
    /** The ID for the request */
 | 
			
		||||
    public string $id;
 | 
			
		||||
 | 
			
		||||
    /** The date/time the request was originally entered */
 | 
			
		||||
    public \DateTimeImmutable $enteredOn;
 | 
			
		||||
 | 
			
		||||
    /** The ID of the user to whom this request belongs */
 | 
			
		||||
    public string $userId = '';
 | 
			
		||||
 | 
			
		||||
    /** The date/time the snooze expires for this request */
 | 
			
		||||
    public ?\DateTimeImmutable $snoozedUntil = null;
 | 
			
		||||
 | 
			
		||||
    /** The date/time this request should once again show as defined by recurrence */
 | 
			
		||||
    public ?\DateTimeImmutable $showAfter = null;
 | 
			
		||||
 | 
			
		||||
    /** The type of recurrence for this request */
 | 
			
		||||
    public RecurrenceType $recurrenceType = RecurrenceType::Immediate;
 | 
			
		||||
 | 
			
		||||
    /** The units which apply to recurrences other than Immediate */
 | 
			
		||||
    public ?int $recurrence = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The history for this request
 | 
			
		||||
     * @var History[] $history
 | 
			
		||||
     */
 | 
			
		||||
    public array $history = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The notes for this request
 | 
			
		||||
     * @var Note[] $notes
 | 
			
		||||
     */
 | 
			
		||||
    public array $notes = [];
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->id = new Cuid2();
 | 
			
		||||
        $this->enteredOn = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								src/app/domain/RequestAction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/app/domain/RequestAction.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace MyPrayerJournal\Domain;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An action that was taken on a request
 | 
			
		||||
 */
 | 
			
		||||
enum RequestAction implements \JsonSerializable
 | 
			
		||||
{
 | 
			
		||||
    /** The request was entered */
 | 
			
		||||
    case Created;
 | 
			
		||||
 | 
			
		||||
    /** Prayer was recorded for the request */
 | 
			
		||||
    case Prayed;
 | 
			
		||||
 | 
			
		||||
    /** The request was updated */
 | 
			
		||||
    case Updated;
 | 
			
		||||
 | 
			
		||||
    /** The request was marked as answered */
 | 
			
		||||
    case Answered;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Serialize this enum using its name
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonSerialize(): mixed
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								src/app/index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/app/index.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require __DIR__ . '/vendor/autoload.php';
 | 
			
		||||
 | 
			
		||||
use MyPrayerJournal\Data;
 | 
			
		||||
 | 
			
		||||
Data::startUp();
 | 
			
		||||
 | 
			
		||||
app()->template->config('path', './pages');
 | 
			
		||||
app()->template->config('params', [
 | 
			
		||||
    // 'app'       => function () { return app(); },
 | 
			
		||||
    'page_link' => function (string $url, bool $checkActive = false) {
 | 
			
		||||
       echo 'href="'. $url . '" hx-get="' . $url . '"';
 | 
			
		||||
       if ($checkActive && str_starts_with($_SERVER['REQUEST_URI'], $url)) {
 | 
			
		||||
           echo ' class="is-active-route"';
 | 
			
		||||
       }
 | 
			
		||||
       echo 'hx-target="#top" hx-swap="innerHTML" hx-push-url="true"';
 | 
			
		||||
    },
 | 
			
		||||
    'version' => 'v4',
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
function renderPage(string $template, array $params, string $pageTitle)
 | 
			
		||||
{
 | 
			
		||||
    if (is_null($params)) {
 | 
			
		||||
        $params = [];
 | 
			
		||||
    }
 | 
			
		||||
    $params['pageTitle'] = $pageTitle;
 | 
			
		||||
    $params['isHtmx'] =
 | 
			
		||||
        array_key_exists('HTTP_HX_REQUEST', $_SERVER)
 | 
			
		||||
            && (!array_key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER));
 | 
			
		||||
    $params['userId'] = false;
 | 
			
		||||
    $params['pageContent'] = app()->template->render($template, $params);
 | 
			
		||||
    // TODO: make the htmx distinction here
 | 
			
		||||
    response()->markup(app()->template->render('layout/full', $params));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app()->get('/', function () {
 | 
			
		||||
    renderPage('home', [], 'Welcome');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app()->get('/legal/privacy-policy', function () {
 | 
			
		||||
    renderPage('legal/privacy-policy', [], 'Privacy Policy');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app()->get('/legal/terms-of-service', function () {
 | 
			
		||||
    renderPage('legal/terms-of-service', [], 'Terms of Service');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app()->run();
 | 
			
		||||
							
								
								
									
										13
									
								
								src/app/pages/home.view.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/app/pages/home.view.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
<article class="container mt-3">
 | 
			
		||||
    <p> </p>
 | 
			
		||||
    <p>
 | 
			
		||||
        myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
 | 
			
		||||
        update them as God moves in the situation, and record a final answer received on that request. It also allows
 | 
			
		||||
        individuals to review their answered prayers.
 | 
			
		||||
    </p>
 | 
			
		||||
    <p>
 | 
			
		||||
        This site is open and available to the general public. To get started, simply click the “Log On”
 | 
			
		||||
        link above, and log on with either a Microsoft or Google account. You can also learn more about the site at the
 | 
			
		||||
        “Docs” link, also above.
 | 
			
		||||
    </p>
 | 
			
		||||
</article>
 | 
			
		||||
							
								
								
									
										30
									
								
								src/app/pages/layout/_foot.view.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/app/pages/layout/_foot.view.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
<?php
 | 
			
		||||
if (!$isHtmx) { ?>
 | 
			
		||||
    <footer class="container-fluid">
 | 
			
		||||
        <p class="text-muted text-end">
 | 
			
		||||
            myPrayerJournal <?= $version ?><br>
 | 
			
		||||
            <em><small>
 | 
			
		||||
                <a <?php $page_link('/legal/privacy-policy'); ?>>Privacy Policy</a> •
 | 
			
		||||
                <a <?php $page_link('/legal/terms-of-service'); ?>>Terms of Service</a> •
 | 
			
		||||
                <a href="https://github.com/bit-badger/myprayerjournal" target="_blank" rel="noopener">Developed</a>
 | 
			
		||||
                and hosted by
 | 
			
		||||
                <a href="https://bitbadger.solutions" target="_blank" rel="noopener">Bit Badger Solutions</a>
 | 
			
		||||
            </small></em>
 | 
			
		||||
        </p>
 | 
			
		||||
        <script src="https://unpkg.com/htmx.org@1.9.4"
 | 
			
		||||
                integrity="sha384-zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV"
 | 
			
		||||
                crossorigin="anonymous"></script>
 | 
			
		||||
        <!-- script [] [
 | 
			
		||||
            rawText "if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')"
 | 
			
		||||
        ] -->
 | 
			
		||||
        <script async src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
 | 
			
		||||
                integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
 | 
			
		||||
                crossorigin="anonymous"></script>
 | 
			
		||||
        <!-- script [] [
 | 
			
		||||
            rawText "setTimeout(function () { "
 | 
			
		||||
            rawText "if (!bootstrap) document.write('<script src=\"/script/bootstrap.bundle.min.js\"><\/script>') "
 | 
			
		||||
            rawText "}, 2000)"
 | 
			
		||||
        ] -->
 | 
			
		||||
        <script src="/script/mpj.js"></script>
 | 
			
		||||
    </footer><?php
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								src/app/pages/layout/_head.view.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/app/pages/layout/_head.view.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
<head>
 | 
			
		||||
    <title><?php echo htmlentities($pageTitle); ?> « myPrayerJournal</title><?php
 | 
			
		||||
    if (!$isHtmx) { ?>
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
        <meta name="description" content="Online prayer journal - free w/Google or Microsoft account">
 | 
			
		||||
        <link href= "https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
 | 
			
		||||
              integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
 | 
			
		||||
              crossorigin="anonymous">
 | 
			
		||||
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
 | 
			
		||||
        <link href="/style/style.css" rel="stylesheet"><?php
 | 
			
		||||
    } ?>
 | 
			
		||||
</head>
 | 
			
		||||
							
								
								
									
										21
									
								
								src/app/pages/layout/_nav.view.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/app/pages/layout/_nav.view.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
<nav class="navbar navbar-dark" role="navigation">
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
        <a <?php $page_link('/'); ?> class="navbar-brand">
 | 
			
		||||
            <span class="m">my</span><span class="p">Prayer</span><span class="j">Journal</span>
 | 
			
		||||
        </a>
 | 
			
		||||
        <ul class="navbar-nav me-auto d-flex flex-row"><?php
 | 
			
		||||
            if ($userId) { ?>
 | 
			
		||||
                <li class="nav-item"><a <?php $page_link('/journal', true); ?>>Journal</a></li>
 | 
			
		||||
                <li class="nav-item"><a <?php $page_link('/requests/active', true); ?>>Active</a></li><?php
 | 
			
		||||
                if ($hasSnoozed) { ?>
 | 
			
		||||
                    <li class="nav-item"><a <?php $page_link('/requests/snoozed', true); ?>>Snoozed</a></li><?php
 | 
			
		||||
                } ?>
 | 
			
		||||
                <li class="nav-item"><a <?php $page_link('/requests/answered', true); ?>>Answered</a></li>
 | 
			
		||||
                <li class="nav-item"><a href="/user/log-off">Log Off</a></li><?php
 | 
			
		||||
            } else { ?>
 | 
			
		||||
                <li class="nav-item"><a href="/user/log-on">Log On</a></li><?php
 | 
			
		||||
            } ?>
 | 
			
		||||
            <li class="nav-item"><a href="https://docs.prayerjournal.me" target="_blank" rel="noopener">Docs</a></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
</nav>
 | 
			
		||||
							
								
								
									
										11
									
								
								src/app/pages/layout/full.view.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/app/pages/layout/full.view.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<?php echo app()->template->render('layout/_head', [ 'pageTitle' => $pageTitle, 'isHtmx' => $isHtmx ]); ?>
 | 
			
		||||
<body>
 | 
			
		||||
    <section id="top" aria-label="Top navigation">
 | 
			
		||||
        <?php echo app()->template->render('layout/_nav', [ 'userId' => $userId ]); ?>
 | 
			
		||||
        <?php echo $pageContent; ?>
 | 
			
		||||
    </section>
 | 
			
		||||
    <?php echo app()->template->render('layout/_foot', [ 'isHtmx' => $isHtmx ]); ?>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										82
									
								
								src/app/pages/legal/privacy-policy.view.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/app/pages/legal/privacy-policy.view.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
			
		||||
<article class="container mt-3">
 | 
			
		||||
    <h2 class="mb-2">Privacy Policy</h2>
 | 
			
		||||
    <h6 class="text-muted pb-3">as of May 21<sup>st</sup>, 2018</h6>
 | 
			
		||||
    <p>
 | 
			
		||||
        The nature of the service is one where privacy is a must. The items below will help you understand the data we
 | 
			
		||||
        collect, access, and store on your behalf as you use this service.
 | 
			
		||||
    </p>
 | 
			
		||||
    <div class="card">
 | 
			
		||||
        <div class="list-group list-group-flush">
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>Third Party Services</h3>
 | 
			
		||||
                <p class="card-text">
 | 
			
		||||
                    myPrayerJournal utilizes a third-party authentication and identity provider. You should familiarize
 | 
			
		||||
                    yourself with the privacy policy for
 | 
			
		||||
                    <a href="https://auth0.com/privacy" target="_blank" rel="noopener">Auth0</a>, as well as
 | 
			
		||||
                    your chosen provider
 | 
			
		||||
                    (<a href="https://privacy.microsoft.com/en-us/privacystatement" target="_blank"
 | 
			
		||||
                        rel="noopener">Microsoft</a> or
 | 
			
		||||
                    <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Google</a>).
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>What We Collect</h3>
 | 
			
		||||
                <h4>Identifying Data</h4>
 | 
			
		||||
                <ul>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        The only identifying data myPrayerJournal stores is the subscriber (“sub”) field
 | 
			
		||||
                        from the token we receive from Auth0, once you have signed in through their hosted service. All
 | 
			
		||||
                        information is associated with you via this field.
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        While you are signed in, within your browser, the service has access to your first and last
 | 
			
		||||
                        names, along with a URL to the profile picture (provided by your selected identity provider).
 | 
			
		||||
                        This information is not transmitted to the server, and is removed when “Log Off” is
 | 
			
		||||
                        clicked.
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                <h4>User Provided Data</h4>
 | 
			
		||||
                <ul class="mb-0">
 | 
			
		||||
                    <li>
 | 
			
		||||
                        myPrayerJournal stores the information you provide, including the text of prayer requests,
 | 
			
		||||
                        updates, and notes; and the date/time when certain actions are taken.
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>How Your Data Is Accessed / Secured</h3>
 | 
			
		||||
                <ul class="mb-0">
 | 
			
		||||
                    <li>
 | 
			
		||||
                        Your provided data is returned to you, as required, to display your journal or your answered
 | 
			
		||||
                        requests. On the server, it is stored in a controlled-access database.
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        Your data is backed up, along with other Bit Badger Solutions hosted systems, in a rolling
 | 
			
		||||
                        manner; backups are preserved for the prior 7 days, and backups from the 1<sup>st</sup> and
 | 
			
		||||
                        15<sup>th</sup> are preserved for 3 months. These backups are stored in a private cloud data
 | 
			
		||||
                        repository.
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        The data collected and stored is the absolute minimum necessary for the functionality of the
 | 
			
		||||
                        service. There are no plans to “monetize” this service, and storing the minimum
 | 
			
		||||
                        amount of information means that the data we have is not interesting to purchasers (or those who
 | 
			
		||||
                        may have more nefarious purposes).
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        Access to servers and backups is strictly controlled and monitored for unauthorized access
 | 
			
		||||
                        attempts.
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>Removing Your Data</h3>
 | 
			
		||||
                <p class="card-text">
 | 
			
		||||
                    At any time, you may choose to discontinue using this service. Both Microsoft and Google provide
 | 
			
		||||
                    ways to revoke access from this application. However, if you want your data removed from the
 | 
			
		||||
                    database, please contact daniel at bitbadger.solutions (via e-mail, replacing at with @) prior to
 | 
			
		||||
                    doing so, to ensure we can determine which subscriber ID belongs to you.
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</article>
 | 
			
		||||
							
								
								
									
										57
									
								
								src/app/pages/legal/terms-of-service.view.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/app/pages/legal/terms-of-service.view.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
<article class="container mt-3">
 | 
			
		||||
    <h2 class="mb-2">Terms of Service</h2>
 | 
			
		||||
    <h6 class="text-muted pb-3">as of May 21<sup>st</sup>, 2018</h6>
 | 
			
		||||
    <div class="card">
 | 
			
		||||
        <div class="list-group list-group-flush">
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>1. Acceptance of Terms</h3>
 | 
			
		||||
                <p class="card-text">
 | 
			
		||||
                    By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you
 | 
			
		||||
                    are responsible to ensure that your use of this site complies with all applicable laws. Your
 | 
			
		||||
                    continued use of this site implies your acceptance of these terms.
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>2. Description of Service and Registration</h3>
 | 
			
		||||
                <p class="card-text">
 | 
			
		||||
                    myPrayerJournal is a service that allows individuals to enter and amend their prayer requests. It
 | 
			
		||||
                    requires no registration by itself, but access is granted based on a successful login with an
 | 
			
		||||
                    external identity provider. See
 | 
			
		||||
                    <a <?php $page_link('/legal/privacy-policy'); ?>>our privacy policy</a> for details on how that
 | 
			
		||||
                    information is accessed and stored.
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>3. Third Party Services</h3>
 | 
			
		||||
                <p class="card-text">
 | 
			
		||||
                    This service utilizes a third-party service provider for identity management. Review the terms of
 | 
			
		||||
                    service for <a href="https://auth0.com/terms" target="_blank" rel="noopener">Auth0</a>, as well as
 | 
			
		||||
                    those for the selected authorization provider
 | 
			
		||||
                    (<a href="https://www.microsoft.com/en-us/servicesagreement" target="_blank"
 | 
			
		||||
                        rel="noopener">Microsoft</a> or
 | 
			
		||||
                    <a href="https://policies.google.com/terms" target="_blank" rel="noopener">Google</a>).
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>4. Liability</h3>
 | 
			
		||||
                <p class="card-text">
 | 
			
		||||
                    This service is provided “as is”, and no warranty (express or implied) exists. The
 | 
			
		||||
                    service and its developers may not be held liable for any damages that may arise through the use of
 | 
			
		||||
                    this service.
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <h3>5. Updates to Terms</h3>
 | 
			
		||||
                <p class="card-text">
 | 
			
		||||
                    These terms and conditions may be updated at any time, and this service does not have the capability
 | 
			
		||||
                    to notify users when these change. The date at the top of the page will be updated when any of the
 | 
			
		||||
                    text of these terms is updated.
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <p class="pt-3">
 | 
			
		||||
        You may also wish to review our <a <?php $page_link('/legal/privacy-policy'); ?>>privacy policy</a> to learn how
 | 
			
		||||
        we handle your data.
 | 
			
		||||
    </p>
 | 
			
		||||
</article>
 | 
			
		||||
							
								
								
									
										104
									
								
								src/app/script/mpj.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/app/script/mpj.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
			
		||||
"use strict"
 | 
			
		||||
 | 
			
		||||
/** myPrayerJournal script */
 | 
			
		||||
this.mpj = {
 | 
			
		||||
  /**
 | 
			
		||||
   * Show a message via toast
 | 
			
		||||
   * @param {string} message The message to show
 | 
			
		||||
   */
 | 
			
		||||
  showToast (message) {
 | 
			
		||||
    const [level, msg] = message.split("|||")
 | 
			
		||||
    
 | 
			
		||||
    let header
 | 
			
		||||
    if (level !== "success") {
 | 
			
		||||
      const heading = typ => `<span class="me-auto"><strong>${typ.toUpperCase()}</strong></span>`
 | 
			
		||||
      
 | 
			
		||||
      header = document.createElement("div")
 | 
			
		||||
      header.className = "toast-header"
 | 
			
		||||
      header.innerHTML = heading(level === "warning" ? level : "error")
 | 
			
		||||
      
 | 
			
		||||
      const close = document.createElement("button")
 | 
			
		||||
      close.type = "button"
 | 
			
		||||
      close.className = "btn-close"
 | 
			
		||||
      close.setAttribute("data-bs-dismiss", "toast")
 | 
			
		||||
      close.setAttribute("aria-label", "Close")
 | 
			
		||||
      header.appendChild(close)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const body = document.createElement("div")
 | 
			
		||||
    body.className = "toast-body"
 | 
			
		||||
    body.innerText = msg
 | 
			
		||||
    
 | 
			
		||||
    const toastEl = document.createElement("div")
 | 
			
		||||
    toastEl.className = `toast bg-${level === "error" ? "danger" : level} text-white`
 | 
			
		||||
    toastEl.setAttribute("role", "alert")
 | 
			
		||||
    toastEl.setAttribute("aria-live", "assertlive")
 | 
			
		||||
    toastEl.setAttribute("aria-atomic", "true")
 | 
			
		||||
    toastEl.addEventListener("hidden.bs.toast", e => e.target.remove())
 | 
			
		||||
    if (header) toastEl.appendChild(header)
 | 
			
		||||
    
 | 
			
		||||
    toastEl.appendChild(body)
 | 
			
		||||
    document.getElementById("toasts").appendChild(toastEl)
 | 
			
		||||
    new bootstrap.Toast(toastEl, { autohide: level === "success" }).show()
 | 
			
		||||
  },
 | 
			
		||||
  /**
 | 
			
		||||
   * Load local version of Bootstrap CSS if the CDN load failed
 | 
			
		||||
   */
 | 
			
		||||
  ensureCss () {
 | 
			
		||||
    let loaded = false
 | 
			
		||||
    for (let i = 0; !loaded && i < document.styleSheets.length; i++) {
 | 
			
		||||
      loaded = document.styleSheets[i].href.endsWith("bootstrap.min.css")
 | 
			
		||||
    }
 | 
			
		||||
    if (!loaded) {
 | 
			
		||||
      const css = document.createElement("link")
 | 
			
		||||
      css.rel = "stylesheet"
 | 
			
		||||
      css.href = "/style/bootstrap.min.css"
 | 
			
		||||
      document.getElementsByTagName("head")[0].appendChild(css)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  /** Script for the request edit component */
 | 
			
		||||
  edit: {
 | 
			
		||||
    /**
 | 
			
		||||
     * Toggle the recurrence input fields
 | 
			
		||||
     * @param {Event} e The click event
 | 
			
		||||
     */
 | 
			
		||||
    toggleRecurrence ({ target }) {
 | 
			
		||||
      const isDisabled = target.value === "Immediate"
 | 
			
		||||
      ;["recurCount", "recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  /**
 | 
			
		||||
   * The time zone of the current browser
 | 
			
		||||
   * @type {string}
 | 
			
		||||
   **/
 | 
			
		||||
  timeZone: undefined,
 | 
			
		||||
  /**
 | 
			
		||||
   * Derive the time zone from the current browser
 | 
			
		||||
   */
 | 
			
		||||
  deriveTimeZone () {
 | 
			
		||||
    try {
 | 
			
		||||
      this.timeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone
 | 
			
		||||
    } catch (_) { }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
htmx.on("htmx:afterOnLoad", function (evt) {
 | 
			
		||||
  const hdrs = evt.detail.xhr.getAllResponseHeaders()
 | 
			
		||||
  // Show a message if there was one in the response
 | 
			
		||||
  if (hdrs.indexOf("x-toast") >= 0) {
 | 
			
		||||
    mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
 | 
			
		||||
  }
 | 
			
		||||
  // Hide a modal window if requested
 | 
			
		||||
  if (hdrs.indexOf("x-hide-modal") >= 0) {
 | 
			
		||||
    document.getElementById(evt.detail.xhr.getResponseHeader("x-hide-modal") + "Dismiss").click()
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
htmx.on("htmx:configRequest", function (evt) {
 | 
			
		||||
  // Send the user's current time zone so that we can display local time
 | 
			
		||||
  if (mpj.timeZone) {
 | 
			
		||||
    evt.detail.headers["X-Time-Zone"] = mpj.timeZone
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
mpj.deriveTimeZone()
 | 
			
		||||
							
								
								
									
										60
									
								
								src/app/style/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/app/style/style.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
			
		||||
 | 
			
		||||
nav {
 | 
			
		||||
  background-color: green;
 | 
			
		||||
  
 | 
			
		||||
  & .m {
 | 
			
		||||
    font-weight: 100;
 | 
			
		||||
  }
 | 
			
		||||
  & .p {
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
  }
 | 
			
		||||
  & .j {
 | 
			
		||||
    font-weight: 700;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.nav-item {
 | 
			
		||||
  & a:link,
 | 
			
		||||
  & a:visited {
 | 
			
		||||
    padding: .5rem 1rem;
 | 
			
		||||
    margin: 0 .5rem;
 | 
			
		||||
    border-radius: .5rem;
 | 
			
		||||
    color: white;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
  }
 | 
			
		||||
  & a:hover {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    background-color: rgba(255, 255, 255, .2);
 | 
			
		||||
  }
 | 
			
		||||
  & a.is-active-route {
 | 
			
		||||
    border-top-left-radius: 0;
 | 
			
		||||
    border-top-right-radius: 0;
 | 
			
		||||
    border-top: solid 4px rgba(255, 255, 255, .3);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
form {
 | 
			
		||||
  max-width: 60rem;
 | 
			
		||||
  margin: auto;
 | 
			
		||||
}
 | 
			
		||||
.action-cell .material-icons {
 | 
			
		||||
  font-size: 1.1rem ;
 | 
			
		||||
}
 | 
			
		||||
.material-icons {
 | 
			
		||||
  vertical-align: bottom;
 | 
			
		||||
}
 | 
			
		||||
#toastHost {
 | 
			
		||||
  position: sticky;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
.request-text {
 | 
			
		||||
  white-space: pre-line
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
footer {
 | 
			
		||||
  border-top: solid 1px lightgray;
 | 
			
		||||
  margin: 1rem -1rem 0;
 | 
			
		||||
  padding: 0 1rem;
 | 
			
		||||
  
 | 
			
		||||
  & p {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user