From 608f775f240a87ed05575fde961b309868baf9e7 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Sun, 16 Jun 2024 14:02:34 +0000 Subject: [PATCH] refactor: streamlined all API endpoints and remove local API packages (#36) * wip: 2024-06-13T07:32:13+0200 (1718256733) * wip: 2024-06-16T13:05:34+0200 (1718535934) * wip: 2024-06-16T15:07:31+0200 (1718543251) --- api/composer.json | 12 +- api/composer.lock | 71 +++--- api/endpoints/coffee/GET.php | 39 --- api/endpoints/coffee/POST.php | 36 --- api/endpoints/media/GET.php | 95 -------- api/endpoints/media/POST.php | 117 --------- api/endpoints/media/srcset/GET.php | 106 --------- api/endpoints/media/srcset/POST.php | 55 ----- api/endpoints/messages/POST.php | 49 +--- api/endpoints/releases/POST.php | 223 ------------------ api/endpoints/search/GET.php | 116 ++------- api/endpoints/work/DELETE.php | 66 +++--- api/endpoints/work/GET.php | 146 ++++-------- api/endpoints/work/PATCH.php | 180 ++++---------- api/endpoints/work/POST.php | 128 +++++----- api/endpoints/work/actions/DELETE.php | 48 +--- api/endpoints/work/actions/GET.php | 39 +-- api/endpoints/work/actions/POST.php | 79 +++---- api/endpoints/work/permalinks/GET.php | 49 ++-- api/endpoints/work/permalinks/POST.php | 71 ++---- api/endpoints/work/tags/DELETE.php | 61 +---- api/endpoints/work/tags/GET.php | 48 ++++ api/endpoints/work/tags/POST.php | 89 +++---- api/src/Endpoints.php | 17 ++ api/src/databases/VLWdb.php | 22 +- api/src/databases/models/Coffee.php | 10 - api/src/databases/models/Media.php | 24 -- api/src/databases/models/MediaSrcset.php | 10 - api/src/databases/models/Messages.php | 15 -- .../databases/models/Messages/Messages.php | 15 ++ api/src/databases/models/Work.php | 19 -- api/src/databases/models/Work/Work.php | 19 ++ .../models/{ => Work}/WorkActions.php | 3 +- .../databases/models/{ => Work}/WorkMedia.php | 0 .../models/{ => Work}/WorkPermalinks.php | 2 +- api/src/databases/models/Work/WorkTags.php | 20 ++ api/src/databases/models/WorkTags.php | 20 -- api/src/packages/Endpoints/composer.json | 17 -- api/src/packages/Endpoints/src/Endpoints.php | 11 - composer.json | 16 +- composer.lock | 100 ++++---- pages/contact.php | 24 +- pages/search.php | 176 ++++++-------- pages/work.php | 111 +++++---- .../API/src/Client.php => client/API.php} | 6 +- src/entities/Entity.php | 57 +++++ src/entities/VLW/Work.php | 74 ++++++ src/packages/API/composer.json | 20 -- src/packages/API/composer.lock | 58 ----- 49 files changed, 845 insertions(+), 1944 deletions(-) delete mode 100755 api/endpoints/coffee/GET.php delete mode 100755 api/endpoints/coffee/POST.php delete mode 100755 api/endpoints/media/GET.php delete mode 100755 api/endpoints/media/POST.php delete mode 100755 api/endpoints/media/srcset/GET.php delete mode 100755 api/endpoints/media/srcset/POST.php delete mode 100755 api/endpoints/releases/POST.php create mode 100644 api/endpoints/work/tags/GET.php create mode 100644 api/src/Endpoints.php delete mode 100755 api/src/databases/models/Coffee.php delete mode 100755 api/src/databases/models/Media.php delete mode 100755 api/src/databases/models/MediaSrcset.php delete mode 100755 api/src/databases/models/Messages.php create mode 100644 api/src/databases/models/Messages/Messages.php delete mode 100755 api/src/databases/models/Work.php create mode 100644 api/src/databases/models/Work/Work.php rename api/src/databases/models/{ => Work}/WorkActions.php (80%) mode change 100755 => 100644 rename api/src/databases/models/{ => Work}/WorkMedia.php (100%) mode change 100755 => 100644 rename api/src/databases/models/{ => Work}/WorkPermalinks.php (78%) mode change 100755 => 100644 create mode 100644 api/src/databases/models/Work/WorkTags.php delete mode 100755 api/src/databases/models/WorkTags.php delete mode 100644 api/src/packages/Endpoints/composer.json delete mode 100644 api/src/packages/Endpoints/src/Endpoints.php rename src/{packages/API/src/Client.php => client/API.php} (70%) create mode 100644 src/entities/Entity.php create mode 100644 src/entities/VLW/Work.php delete mode 100644 src/packages/API/composer.json delete mode 100644 src/packages/API/composer.lock diff --git a/api/composer.json b/api/composer.json index 4e00ddc..e1058f7 100755 --- a/api/composer.json +++ b/api/composer.json @@ -1,14 +1,8 @@ { "require": { - "local/api.endpoints": "1.0.0-dev", "reflect/plugin-rules": "^1.5", - "victorwesterlund/xenum": "^1.1" + "victorwesterlund/xenum": "dev-master", + "victorwesterlund/libmysqldriver": "dev-master" }, - "minimum-stability": "dev", - "repositories": [ - { - "type": "path", - "url": "src/packages/Endpoints" - } - ] + "minimum-stability": "dev" } diff --git a/api/composer.lock b/api/composer.lock index aaeccbf..2b6ee6b 100755 --- a/api/composer.lock +++ b/api/composer.lock @@ -4,33 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9da96ba90ef20d885034442b30dce0a3", + "content-hash": "13ba5cc60bab24ac8ef5b1018fe4249b", "packages": [ - { - "name": "local/api.endpoints", - "version": "1.0.0-dev", - "dist": { - "type": "path", - "url": "src/packages/Endpoints", - "reference": "89b7b9a4cc504abddb4aeec8e05a95c9d9087575" - }, - "type": "library", - "autoload": { - "psr-4": { - "VLW\\API\\": "src/" - } - }, - "authors": [ - { - "name": "Victor Westerlund", - "email": "victor@vlw.se" - } - ], - "description": "Endpoint pathmappings for VLW API", - "transport-options": { - "relative": true - } - }, { "name": "reflect/plugin-rules", "version": "1.5.0", @@ -68,9 +43,47 @@ }, "time": "2024-01-17T11:07:44+00:00" }, + { + "name": "victorwesterlund/libmysqldriver", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/VictorWesterlund/php-libmysqldriver.git", + "reference": "adc2fda90a3b8308e8a9df202d5ec418a9220ff8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/VictorWesterlund/php-libmysqldriver/zipball/adc2fda90a3b8308e8a9df202d5ec418a9220ff8", + "reference": "adc2fda90a3b8308e8a9df202d5ec418a9220ff8", + "shasum": "" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "libmysqldriver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Victor Westerlund", + "email": "victor.vesterlund@gmail.com" + } + ], + "description": "Abstraction library for common mysqli features", + "support": { + "issues": "https://github.com/VictorWesterlund/php-libmysqldriver/issues", + "source": "https://github.com/VictorWesterlund/php-libmysqldriver/tree/3.6.1" + }, + "time": "2024-04-29T08:17:12+00:00" + }, { "name": "victorwesterlund/xenum", - "version": "1.1.1", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/VictorWesterlund/php-xenum.git", @@ -82,6 +95,7 @@ "reference": "8972f06f42abd1f382807a67e937d5564bb89699", "shasum": "" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -110,7 +124,8 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "local/api.endpoints": 20 + "victorwesterlund/xenum": 20, + "victorwesterlund/libmysqldriver": 20 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/api/endpoints/coffee/GET.php b/api/endpoints/coffee/GET.php deleted file mode 100755 index 4ee9144..0000000 --- a/api/endpoints/coffee/GET.php +++ /dev/null @@ -1,39 +0,0 @@ -db->for(CoffeeModel::TABLE) - ->order([CoffeeModel::DATE_TIMESTAMP_CREATED->value => "DESC"]) - ->limit(self::LIST_LIMIT) - ->select([ - CoffeeModel::ID->value, - CoffeeModel::DATE_TIMESTAMP_CREATED->value - ]); - - return parent::is_mysqli_result($resp) - ? new Response($resp->fetch_all(MYSQLI_ASSOC)) - : $this->resp_database_error(); - } - } \ No newline at end of file diff --git a/api/endpoints/coffee/POST.php b/api/endpoints/coffee/POST.php deleted file mode 100755 index 5643942..0000000 --- a/api/endpoints/coffee/POST.php +++ /dev/null @@ -1,36 +0,0 @@ -db->for(CoffeeModel::TABLE) - ->insert([ - CoffeeModel::ID->value => $id, - CoffeeModel::DATE_TIMESTAMP_CREATED->value => time(), - ]); - - // Return 201 Created and entity id if successful - return $insert ? new Response($id, 201) : $this->resp_database_error(); - } - } \ No newline at end of file diff --git a/api/endpoints/media/GET.php b/api/endpoints/media/GET.php deleted file mode 100755 index bda4957..0000000 --- a/api/endpoints/media/GET.php +++ /dev/null @@ -1,95 +0,0 @@ -ruleset = new Ruleset(strict: true); - - $this->ruleset->GET([ - (new Rules(MediaModel::ID->value)) - ->required() - ->type(Type::STRING) - ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), - - (new Rules(self::GET_DISPOSITION_KEY)) - ->type(Type::ENUM, MediaDispositionEnum::values()) - ->default(MediaDispositionEnum::METADATA->value) - ]); - } - - // # Helper methods - - private function fetch_srcset(string $id): array { - $resp = $this->db->for(WorkTagsModel::TABLE) - ->where([WorkTagsModel::ANCHOR->value => $id]) - ->select(WorkTagsModel::NAME->value); - - return parent::is_mysqli_result($resp) ? $resp->fetch_all(MYSQLI_ASSOC) : []; - } - - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); - } - - public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - $resp = $this->db->for(MediaModel::TABLE) - ->where([MediaModel::ID->value => $_GET[MediaModel::ID->value]]) - ->select([ - MediaModel::ID->value, - MediaModel::NAME->value, - MediaModel::TYPE->value, - MediaModel::MIME->value, - MediaModel::EXTENSION->value, - MediaModel::SRCSET->value, - MediaModel::DATE_TIMESTAMP_CREATED->value, - ]); - - // Bail out if something went wrong retrieving rows from the database - if (!parent::is_mysqli_result($resp)) { - return $this->resp_database_error(); - } - - $media = $resp->fetch_assoc(); - $test = true; - } - } \ No newline at end of file diff --git a/api/endpoints/media/POST.php b/api/endpoints/media/POST.php deleted file mode 100755 index 1bd7916..0000000 --- a/api/endpoints/media/POST.php +++ /dev/null @@ -1,117 +0,0 @@ -ruleset = new Ruleset(strict: true); - - $this->ruleset->POST([ - (new Rules(MediaModel::ID->value)) - ->type(Type::STRING) - ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ->default(parent::gen_uuid4()), - - (new Rules(MediaModel::NAME->value)) - ->type(Type::STRING) - ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ->default(null), - - (new Rules(MediaModel::TYPE->value)) - ->type(Type::ENUM, MediaTypeEnum::values()) - ->default(null), - - (new Rules(MediaModel::EXTENSION->value)) - ->type(Type::STRING) - ->min(3) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ->default(null), - - (new Rules(MediaModel::MIME->value)) - ->type(Type::STRING) - ->min(3) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ->default(null), - - (new Rules(MediaModel::SRCSET->value)) - ->type(Type::STRING) - ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ->default(null) - ]); - } - - // # Helper methods - - // Returns true if an srcset exists for provided key - private static function media_srcset_exists(): bool { - // No srcet get parameter has been set - if (empty($_POST[MediaModel::SRCSET->value])) { - return true; - } - - // Check if the provided srcset exists by calling the srcset endpoint - return Call("media/srcset?id={$_POST[MediaModel::SRCSET->value]}", Method::GET)->ok; - } - - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); - } - - public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - // Bail out if an srcset doesn't exist - if (!self::media_srcset_exists()) { - return new Response("No media srcset exists with id '{$_POST[MediaModel::SRCSET->value]}'", 404); - } - - $insert = $this->db->for(MediaModel::TABLE) - ->insert([ - MediaModel::ID->value => $_POST[MediaModel::ID->value], - MediaModel::NAME->value => $_POST[MediaModel::NAME->value], - MediaModel::MIME->value => $_POST[MediaModel::MIME->value], - // Strip dots from extension string if set - MediaModel::EXTENSION->value => $_POST[MediaModel::EXTENSION->value] - ? str_replace(".", "", $_POST[MediaModel::EXTENSION->value]) - : null, - MediaModel::SRCSET->value => $_POST[MediaModel::SRCSET->value], - MediaModel::DATE_TIMESTAMP_CREATED->value => time() - ]); - - // Return media id if insert was successful - return $insert - ? new Response($_POST[MediaModel::ID->value], 201) - : $this->resp_database_error(); - } - } \ No newline at end of file diff --git a/api/endpoints/media/srcset/GET.php b/api/endpoints/media/srcset/GET.php deleted file mode 100755 index f9a0599..0000000 --- a/api/endpoints/media/srcset/GET.php +++ /dev/null @@ -1,106 +0,0 @@ -ruleset = new Ruleset(strict: true); - - $this->ruleset->GET([ - (new Rules(MediaSrcsetModel::ID->value)) - ->required() - ->type(Type::STRING) - ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ]); - } - - // # Helper methods - - // Get metadata for the requested srcset - private function get_srcset(): array|false { - $srcset = $this->db->for(MediaSrcsetModel::TABLE) - ->where([MediaSrcsetModel::ID->value => $_GET[MediaSrcsetModel::ID->value]]) - ->select([MediaSrcsetModel::ANCHOR_DEFAULT->value]); - - // Something went wrong retrieving rows from the database - if (!parent::is_mysqli_result($srcset)) { - return false; - } - - // Return assoc array of srcset data if it exists - return $srcset->num_rows === 1 ? $srcset->fetch_assoc() : false; - } - - // Get all media entities that are part of the requested srcset - private function get_srcset_media(): mysqli_result|false { - $media = $this->db->for(MediaModel::TABLE) - ->where([MediaModel::SRCSET->value => $_GET[MediaSrcsetModel::ID->value]]) - ->select([ - MediaModel::ID->value, - MediaModel::TYPE->value, - MediaModel::MIME->value, - MediaModel::EXTENSION->value - ]); - - return parent::is_mysqli_result($media) ? $media : false; - } - - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); - } - - public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - // Get srcset data - $srcset = $this->get_srcset(); - if (!$srcset) { - return new Response("No media srcset exist with id '{$_GET[MediaSrcsetModel::ID->value]}'", 404); - } - - $media = $this->get_srcset_media(); - if (!$media) { - return new Response("Failed to fetch srcset media", 500); - } - - $media_entities = $media->fetch_all(MYSQLI_ASSOC); - - // This is the id of the media entity that is considered the default or "fallback" - $srcet_default_media_id = $srcset[MediaSrcsetModel::ANCHOR_DEFAULT->value]; - - // Return assoc array of all media entities that are in this srcset - return new Response([ - // Return default media entity separately from the rest of the srcset as an assoc array - "default" => array_filter($media_entities, fn(array $entity) => $entity[MediaModel::ID->value] === $srcet_default_media_id)[0], - // Return all media that isn't default as array of assoc arrays - "srcset" => array_filter($media_entities, fn(array $entity) => $entity[MediaModel::ID->value] !== $srcet_default_media_id) - ]); - } - } \ No newline at end of file diff --git a/api/endpoints/media/srcset/POST.php b/api/endpoints/media/srcset/POST.php deleted file mode 100755 index 609624a..0000000 --- a/api/endpoints/media/srcset/POST.php +++ /dev/null @@ -1,55 +0,0 @@ -code !== 404) { - // Wow a UUID4 collision... buy a lottery ticket - if ($srcset_existing->code === 200) { - return $this->main(); - } - - // Failed to get srcset - return new Response("Something went wrong when checking if the srcset exists", 500); - } - - // Create new srcset entity - $insert = $this->db->for(MediaSrcsetModel::TABLE) - ->insert([ - MediaSrcsetModel::ID->value => $id - ]); - - // Return created srcset id if successful - return $insert - ? new Response($id, 201) - : $this->resp_database_error(); - } - } \ No newline at end of file diff --git a/api/endpoints/messages/POST.php b/api/endpoints/messages/POST.php index e2a7758..5e52b17 100755 --- a/api/endpoints/messages/POST.php +++ b/api/endpoints/messages/POST.php @@ -6,20 +6,16 @@ use ReflectRules\Rules; use ReflectRules\Ruleset; - use Reflect\Method; - use function Reflect\Call; - use VLW\API\Databases\VLWdb\VLWdb; use VLW\API\Databases\VLWdb\Models\Messages\MessagesModel; require_once Path::root("src/databases/VLWdb.php"); - require_once Path::root("src/databases/models/Messages.php"); + require_once Path::root("src/databases/models/Messages/Messages.php"); class POST_Messages extends VLWdb { protected Ruleset $ruleset; public function __construct() { - parent::__construct(); $this->ruleset = new Ruleset(strict: true); $this->ruleset->POST([ @@ -34,46 +30,19 @@ ->min(1) ->max(parent::MYSQL_TEXT_MAX_LENGTH) ]); - } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); + parent::__construct($this->ruleset); } public function main(): Response { - //return new Response(["hello" => "maybe"], 500); + // Use copy of request body as entity + $entity = $_POST; - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } + $entity[MessagesModel::ID->value] = parent::gen_uuid4(); + $entity[MessagesModel::DATE_CREATED->value] = time(); - // Generate UUID for entity - $id = parent::gen_uuid4(); - - // Attempt to create new entity - $insert = $this->db->for(MessagesModel::TABLE) - ->insert([ - MessagesModel::ID->value => $id, - MessagesModel::EMAIL->value => $_POST["email"], - MessagesModel::MESSAGE->value => $_POST["message"], - MessagesModel::DATE_TIMESTAMP_CREATED->value => time(), - ]); - - // Bail out if insert failed - if (!$insert) { - return $this->resp_database_error(); - } - - // Return 201 Created and entity id - return new Response($id, 201); + return $this->db->for(MessagesModel::TABLE)->insert($entity) === true + ? new Response($entity[MessagesModel::ID->value], 201) + : new Response("Failed to create message", 500); } } \ No newline at end of file diff --git a/api/endpoints/releases/POST.php b/api/endpoints/releases/POST.php deleted file mode 100755 index 7160ada..0000000 --- a/api/endpoints/releases/POST.php +++ /dev/null @@ -1,223 +0,0 @@ -ruleset = new Ruleset(strict: true); - - $this->ruleset->POST([ - (new Rules(ReleasesPostModel::GITHUB_USER->value)) - ->required() - ->type(Type::STRING) - ->min(1), - - (new Rules(ReleasesPostModel::GITHUB_REPO->value)) - ->required() - ->type(Type::STRING) - ->min(1), - - (new Rules(ReleasesPostModel::GITHUB_TAG->value)) - ->required() - ->type(Type::STRING) - ->type(Type::NUMBER) - ->min(1) - ]); - - $this->curl = curl_init(); - - curl_setopt($this->curl, CURLOPT_USERAGENT, $_ENV["github"]["user_agent"]); - curl_setopt($this->curl, CURLOPT_HEADER, true); - curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($this->curl, CURLOPT_HTTPHEADER, [ - "Accept" => "application/vnd.github+json", - "Authorization" => "token {$_ENV["github"]["api_key"]}", - "X-GitHub-Api-Version" => "2022-11-28" - ]); - } - - // # GitHub - - // Generate HTML from a GitHub "auto-generate" release body - protected static function gh_auto_release_md_to_html(string $md): string { - $output = ""; - - // Parse each line of markdown - $lines = explode(PHP_EOL, $md); - - foreach ($lines as $i => $line) { - // Ignore header line from releases - if ($i < 1) continue; - - // Replace all URLs with HTMLAnchor tags, they will be PRs - $links = []; - preg_match_all(self::REGEX_URL, $line, $links, PREG_UNMATCHED_AS_NULL); - foreach ($links as $i => $link) { - if (empty($link)) continue; - - // Last crumb from link pathname will be the PR id - $pr_id = explode("/", $link[$i]); - $pr_id = end($pr_id); - - $line = str_replace($link, "{$pr_id}", $line); - } - - // Replace all at-handles with links to GitHub user profiles - $handles = []; - preg_match_all(self::REGEX_HANDLE, $line, $handles, PREG_UNMATCHED_AS_NULL); - foreach ($handles as $i => $handle) { - if (empty($handle)) continue; - - // GitHub user URL without the "@" - $url = "https://github.com/" . substr($handle[$i], 1); - - $line = str_replace($handle, "{$handle[$i]}", $line); - } - - $output .= "

{$line}

"; - } - - return $output; - } - - // Return fully qualified URL to GitHub API releases endpoint - private static function get_url(): string { - return implode("/", [ - self::GITHUB_API, - "repos", - $_POST[ReleasesPostModel::GITHUB_USER->value], - $_POST[ReleasesPostModel::GITHUB_REPO->value], - "releases", - "tags", - $_POST[ReleasesPostModel::GITHUB_TAG->value], - ]); - } - - // Fetch release information from GitHub API - private function fetch_release_data(): array { - $url = self::get_url(); - curl_setopt($this->curl, CURLOPT_URL, self::get_url()); - - $resp = curl_exec($this->curl); - - $header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); - $header = substr($resp, 0, $header_size); - $body = substr($resp, $header_size); - - return json_decode($body, true); - } - - // # Sup - - private function create_link_to_release_page(string $id, string $href): Response { - return Call("work/actions?id={$id}", Method::POST, [ - WorkActionsModel::DISPLAY_TEXT->value => "Release details", - WorkActionsModel::HREF->value => $href, - WorkActionsModel::EXTERNAL->value => true - ]); - } - - // Create a tag for entity - private function create_tag(string $id, WorkTagsNameEnum $tag): Response { - return Call("work/tags?id={$id}", Method::POST, [ - // Set "RELEASE" tag on new entity - WorkTagsModel::NAME->value => $tag->value - ]); - } - - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - $data = $this->fetch_release_data(); - if (!$data) { - return new Response("Failed to fetch release data", 500); - } - - - // Transform repo name to lowercase for summary title - $title = strtolower($_POST["repo"]); - - // Use repo name and tag name as heading for summary - $summary = "

Release {$title}@{$data["name"]}

"; - // Append HTML-ified release notes from GitHub to summary - $summary .= self::gh_auto_release_md_to_html($data["body"]); - - $date_published = new \DateTime($data["published_at"], new \DateTimeZone("UTC")); - - // Create work entity - $work_entity = Call("work", Method::POST, [ - WorkModel::SUMMARY->value => $summary, - // Convert time created to Unix timestamp for work endpoint - WorkModel::DATE_TIMESTAMP_CREATED->value => $date_published->format("U"), - ]); - - // Bail out if creating the work entity failed - if (!$work_entity->ok) { - return new Response("Failed to create work entity for release", 500); - } - - $work_entity_id = $work_entity->output(); - - // Create entity tags for release - $tags = [ - WorkTagsNameEnum::VLW, - WorkTagsNameEnum::RELEASE - ]; - foreach ($tags as $tag) { - // Create entity tag for release or exit if failed to create - if (!$this->create_tag($work_entity_id, $tag)->ok) { - return new Response("Failed to create {$tag->name} tag for release entity", 500); - } - } - - // Create link to release page on GitHub - if (!$this->create_link_to_release_page($work_entity_id, $data["html_url"])) { - return new Response("Failed to create link to release page on GitHub", 500); - } - - return new Response($work_entity_id, 201); - } - } \ No newline at end of file diff --git a/api/endpoints/search/GET.php b/api/endpoints/search/GET.php index 3a39896..5c2c895 100755 --- a/api/endpoints/search/GET.php +++ b/api/endpoints/search/GET.php @@ -11,11 +11,10 @@ use VLW\API\Databases\VLWdb\VLWdb; use VLW\API\Databases\VLWdb\Models\Work\WorkModel; - use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel; + require_once Path::root("src/Endpoints.php"); require_once Path::root("src/databases/VLWdb.php"); - require_once Path::root("src/databases/models/Work.php"); - require_once Path::root("src/databases/models/WorkActions.php"); + require_once Path::root("src/databases/models/Work/Work.php"); class GET_Search extends VLWdb { const GET_QUERY = "q"; @@ -23,118 +22,35 @@ protected Ruleset $ruleset; public function __construct() { - parent::__construct(); $this->ruleset = new Ruleset(strict: true); $this->ruleset->GET([ (new Rules(self::GET_QUERY)) ->required() ->type(Type::STRING) - ->min(2) + ->min(1) ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) ]); + + parent::__construct($this->ruleset); } - // Return an SQL string from array for use in prepared statements - private static function array_to_wildcard_sql(array $columns): string { - $sql = array_map(fn(string $column): string => "{$column} LIKE CONCAT('%', ?, '%')", $columns); - - return implode(" OR ", $sql); - } - - // Return chained AND statements from array for use in prepared statements - private static function array_to_and_statement(array $keys): string { - $sql = array_map(fn(string $k): string => "{$k} = ?", $keys); - - return implode(" AND ", $sql); - } - - // Wildcard search columns in table with query string from query string - // This has to be implemented manually until "libmysqldriver/MySQL" supports wildcard SELECT - private function search(string $table, array $columns, array $conditions = null): array { - // Create CSV from columns array - $columns_concat = implode(",", $columns); - - // Create SQL LIKE wildcard statement for each column. - $where = self::array_to_wildcard_sql($columns); - - // Create array of values from query string for each colum - $values = array_fill(0, count($columns), $_GET[self::GET_QUERY]); - - if ($conditions) { - $conditions_sql = self::array_to_and_statement(array_keys($conditions)); - - // Wrap positive where statements and prepare new group of conditions - // WHERE () AND () - $where = "({$where}) AND ({$conditions_sql})"; - - // Append values from conditions statements to prepared statement - array_push($values, ...array_values($conditions)); - } - - // Order the rows by the array index of $colums received - $rows = $this->db->exec("SELECT {$columns_concat} FROM {$table} WHERE {$where} ORDER BY {$columns_concat}", $values); - // Return results as assoc or empty array - return parent::is_mysqli_result($rows) ? $rows->fetch_all(MYSQLI_ASSOC) : []; - } - - // Search work table - private function search_work(): array { - $search = [ - WorkModel::TITLE->value, - WorkModel::SUMMARY->value, - WorkModel::DATE_TIMESTAMP_CREATED->value, - WorkModel::ID->value - ]; - - $conditions = [ - WorkModel::IS_LISTABLE->value => true - ]; - - $results = $this->search(WorkModel::TABLE, $search, $conditions); - - foreach ($results as &$result) { - $result["actions"] = (new Call(Endpoints::WORK_ACTIONS->value)) - ->params([WorkActionsModel::ANCHOR->value => $result[WorkModel::ID->value]]) - ->get()->output(); - } - - return $results; - } - - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); + private function search_work(): Response { + return (new Call(Endpoints::WORK->value))->params([ + WorkModel::TITLE->value => $_GET[self::GET_QUERY], + WorkModel::SUMMARY->value => $_GET[self::GET_QUERY] + ])->get(); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - // Get search results for each category - $categories = [ - WorkModel::TABLE => $this->search_work() + $results = [ + Endpoints::WORK->value => $this->search_work()->output() ]; - // Count total number of results from all categories - $total_num_results = 0; - foreach (array_values($categories) as $results) { - $total_num_results += count($results); - } + // Calculate the total number of results from all searched endpoints + $num_results = array_sum(array_map(fn(array $result): int => count($result), array_values($results))); - return new Response([ - "query" => $_GET[self::GET_QUERY], - "results" => $categories, - "total_num_results" => $total_num_results - ]); + // Return 404 if no search results + return new Response($results, $num_results > 0 ? 200 : 404); } } \ No newline at end of file diff --git a/api/endpoints/work/DELETE.php b/api/endpoints/work/DELETE.php index d197646..d67c0ce 100755 --- a/api/endpoints/work/DELETE.php +++ b/api/endpoints/work/DELETE.php @@ -1,15 +1,16 @@ ruleset = new Ruleset(strict: true); - $this->ruleset->GET([ - (new Rules("id")) - ->required() + $this->ruleset->POST([ + (new Rules(WorkModel::ID->value)) ->type(Type::STRING) ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(WorkModel::TITLE->value)) + ->type(Type::STRING) + ->min(3) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(WorkModel::SUMMARY->value)) + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_TEXT_MAX_LENGTH), + + (new Rules(WorkModel::IS_LISTABLE->value)) + ->type(Type::BOOLEAN), + + (new Rules(WorkModel::IS_READABLE->value)) + ->type(Type::BOOLEAN), + + (new Rules(WorkModel::DATE_MODIFIED->value)) + ->type(Type::NUMBER) + ->min(1) + ->max(parent::MYSQL_INT_MAX_LENGHT), + + (new Rules(WorkModel::DATE_CREATED->value)) + ->type(Type::NUMBER) + ->min(1) + ->max(parent::MYSQL_INT_MAX_LENGHT) ]); - } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to delete work data, please try again later", 503); + parent::__construct($this->ruleset); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - // Attempt to update the entity - $update = $this->db->for(WorkModel::TABLE) - ->where([WorkModel::ID->value => $_GET["id"]]) - ->update([ - WorkModel::IS_LISTABLE->value => false, - WorkModel::IS_READABLE->value => false - ]); - - return $update ? new Response($_GET["id"]) : $this->resp_database_error(); + return $this->db->for(FieldsEnumsModel::TABLE)->delete($_POST) === true + ? new Response(RESP_DELETE_OK) + : new Response("Failed to delete work entity", 500); } } \ No newline at end of file diff --git a/api/endpoints/work/GET.php b/api/endpoints/work/GET.php index 3a2fe0e..457977e 100755 --- a/api/endpoints/work/GET.php +++ b/api/endpoints/work/GET.php @@ -1,136 +1,94 @@ ruleset = new Ruleset(strict: true); $this->ruleset->GET([ - (new Rules("id")) + (new Rules(WorkModel::ID->value)) ->type(Type::STRING) ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ->default(null) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(WorkModel::TITLE->value)) + ->type(Type::STRING) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(WorkModel::SUMMARY->value)) + ->type(Type::STRING) + ->max(parent::MYSQL_TEXT_MAX_LENGTH), + + (new Rules(WorkModel::IS_LISTABLE->value)) + ->type(Type::BOOLEAN) + ->default(true), + + (new Rules(WorkModel::IS_READABLE->value)) + ->type(Type::BOOLEAN) + ->default(true), + + (new Rules(WorkModel::DATE_MODIFIED->value)) + ->type(Type::NUMBER) + ->min(1) + ->max(parent::MYSQL_INT_MAX_LENGHT), + + (new Rules(WorkModel::DATE_CREATED->value)) + ->type(Type::NUMBER) + ->min(1) + ->max(parent::MYSQL_INT_MAX_LENGHT) ]); - } - // # Helper methods - - private function fetch_row_tags(string $id): array { - $resp = $this->db->for(WorkTagsModel::TABLE) - ->where([WorkTagsModel::ANCHOR->value => $id]) - ->select(WorkTagsModel::NAME->value); - - return parent::is_mysqli_result($resp) ? $resp->fetch_all(MYSQLI_ASSOC) : []; - } - - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); - } - - private function resp_item_details(string $id): Response { - $resp = $this->db->for(WorkModel::TABLE) - ->where([ - WorkModel::ID->value => $id, - WorkModel::IS_READABLE->value => true - ]) - ->limit(1) - ->select([ - WorkModel::ID->value, - WorkModel::TITLE->value, - WorkModel::SUMMARY->value, - WorkModel::COVER_SRCSET->value, - WorkModel::DATE_YEAR->value, - WorkModel::DATE_MONTH->value, - WorkModel::DATE_DAY->value, - WorkModel::DATE_TIMESTAMP_MODIFIED->value, - WorkModel::DATE_TIMESTAMP_CREATED->value - ]); - - // Bail out if something went wrong retrieving rows from the database - if (!parent::is_mysqli_result($resp)) { - return $this->resp_database_error(); - } - - return $resp->num_rows === 1 - ? new Response($resp->fetch_assoc()) - : new Response("No entity with id '{$id}' was found", 404); + parent::__construct($this->ruleset); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); + // Use copy of search paramters as filters + $filters = $_GET; + + // Do a wildcard search on the title column if provided + if (array_key_exists(WorkModel::TITLE->value, $_GET)) { + $filters[WorkModel::TITLE->value] = [ + "LIKE" => "%{$_GET[WorkModel::TITLE->value]}%" + ]; } - // Return details about a specific item by id - if (!empty($_GET["id"])) { - return $this->resp_item_details($_GET["id"]); + // Do a wildcard search on the summary column if provided + if (array_key_exists(WorkModel::SUMMARY->value, $_GET)) { + $filters[WorkModel::SUMMARY->value] = [ + "LIKE" => "%{$_GET[WorkModel::SUMMARY->value]}%" + ]; } - $resp = $this->db->for(WorkModel::TABLE) - ->where([WorkModel::IS_LISTABLE->value => true]) - ->order([WorkModel::DATE_TIMESTAMP_CREATED->value => "DESC"]) + $response = $this->db->for(WorkModel::TABLE) + ->where($filters) ->select([ WorkModel::ID->value, WorkModel::TITLE->value, WorkModel::SUMMARY->value, - WorkModel::COVER_SRCSET->value, + WorkModel::IS_LISTABLE->value, + WorkModel::IS_READABLE->value, WorkModel::DATE_YEAR->value, WorkModel::DATE_MONTH->value, WorkModel::DATE_DAY->value, - WorkModel::DATE_TIMESTAMP_MODIFIED->value, - WorkModel::DATE_TIMESTAMP_CREATED->value + WorkModel::DATE_MODIFIED->value, + WorkModel::DATE_CREATED->value ]); - // Bail out if something went wrong retrieving rows from the database - if (!parent::is_mysqli_result($resp)) { - return $this->resp_database_error(); - } - - // Resolve foreign keys - $rows = []; - while ($row = $resp->fetch_assoc()) { - $row["tags"] = $this->fetch_row_tags($row["id"]); - - // Fetch actions for work entity by id from endpoint - $row["actions"] = (new Call(Endpoints::WORK_ACTIONS->value)) - ->params([WorkActionsModel::ANCHOR->value => $row[WorkModel::ID->value]]) - ->get()->output(); - - $rows[] = $row; - } - - return new Response($rows); + return $response->num_rows > 0 + ? new Response($response->fetch_all(MYSQLI_ASSOC)) + : new Response([], 404); } } \ No newline at end of file diff --git a/api/endpoints/work/PATCH.php b/api/endpoints/work/PATCH.php index e8e9be1..ef2c1ca 100755 --- a/api/endpoints/work/PATCH.php +++ b/api/endpoints/work/PATCH.php @@ -1,30 +1,28 @@ ruleset = new Ruleset(strict: true); $this->ruleset->GET([ @@ -52,19 +50,19 @@ (new Rules(WorkModel::IS_READABLE->value)) ->type(Type::BOOLEAN), - (new Rules(WorkModel::DATE_TIMESTAMP_CREATED->value)) + (new Rules(WorkModel::DATE_MODIFIED->value)) ->type(Type::NUMBER) - ->min(0) + ->min(1) + ->max(parent::MYSQL_INT_MAX_LENGHT) + ->default(time()), + + (new Rules(WorkModel::DATE_CREATED->value)) + ->type(Type::NUMBER) + ->min(1) ->max(parent::MYSQL_INT_MAX_LENGHT) ]); - $this->get_existing_entity(); - - // Copy all provided post data into a new array - $this->updated_entity = $_POST; - - // Set date modified timestamp - $this->updated_entity[WorkModel::DATE_TIMESTAMP_MODIFIED->value] = time(); + parent::__construct(); } // Generate a slug URL from string @@ -72,130 +70,46 @@ return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input))); } - // # Helper methods - - private function get_existing_entity(): Response { - // Check if an entity already exists with slugified title from GET endpoint - $this->current_entity = Call("work?id={$_GET["id"]}", Method::GET); - - // Response is not 404 (Not found) so we can't create the entity - if ($this->current_entity->code !== 200) { - // Response is not a valid entity, something went wrong - if ($this->current_entity->code !== 404) { - return $this->resp_database_error(); - } - - // Return 402 Conflict - return new Response("No entity with id '{$_GET["id"]}' was found", 404); - } - - return $this->current_entity; + // Compute and return modeled year, month, and day from Unix timestamp in request body + private static function gen_date_created(): array { + return [ + WorkModel::DATE_YEAR->value => date("Y", $_POST[WorkModel::DATE_CREATED->value]), + WorkModel::DATE_MONTH ->value => date("n", $_POST[WorkModel::DATE_CREATED->value]), + WorkModel::DATE_DAY->value => date("j", $_POST[WorkModel::DATE_CREATED->value]) + ]; } - // Create new permalink for entity slug - private function create_permalink(string $slug): bool { - $create = Call("work/permalinks", Method::POST, [ - WorkPermalinksModel::SLUG->value => $slug, - WorkPermalinksModel::ANCHOR->value => $slug - ]); - - return $create->ok; - } - - // ## Updated entity - - private function change_slug(): bool { - if (!array_key_exists(WorkModel::ID->value, $this->updated_entity)) { - return true; - } - - // Generate new permalink for entity id - return $this->create_permalink($this->updated_entity[WorkModel::ID->value]); - } - - private function timestamp_to_dates(): void { - if (!array_key_exists(WorkModel::DATE_TIMESTAMP_CREATED->value, $this->updated_entity)) { - return; - } - - // Get timestamp from post data - $timestamp = $this->updated_entity[WorkModel::DATE_TIMESTAMP_CREATED->value]; - - // Update fractured dates from timestamp - $this->updated_entity[WorkModel::DATE_YEAR->value] = date("Y", $timestamp); - $this->updated_entity[WorkModel::DATE_MONTH ->value] = date("n", $timestamp); - $this->updated_entity[WorkModel::DATE_DAY->value] = date("j", $timestamp); - } - - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); - } - - // Return a 422 Unprocessable Entity if there is nothing to change - private function resp_no_changes(): Response { - return new Response("No columns to update", 422); - } - - // Rollback changes and return error response - private function resp_permalink_error_rollback(): Response { - $update = $this->db->for(WorkModel::TABLE) - ->where([WorkModel::ID->value => $_GET["id"]]) - ->update($this->current_entity->output()); - - return $update - ? new Response("Failed to create new permalink for updated entity. Changes have been rolled back", 500) - : new Reponse("Failed to create new permalink for updated entity. Changes failed to rollback, this is bad.", 500); + private function get_entity_by_id(string $id): Response { + return (new Call(Endpoints::WORK->value))->params([ + WorkModel::ID->value => $id + ])->get(); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } + // Use copy of request body as entity + $entity = $_POST; - // Empty payload, nothing to do - if (empty($_POST)) { - return $this->resp_no_changes(); - } + // Generate a new slug id from title if changed + if ($_POST[WorkModel::TITLE->value]) { + $slug = $_POST[WorkModel::TITLE->value]; - // Generate new slug for entity if title is updated - if (array_key_exists(WorkModel::TITLE->value, $_POST)) { - // Generate URL slug from title text or UUID if undefined - $slug = self::gen_slug($_POST["title"]); - - // Save generated slug from title if it's different from existing slug - if ($slug !== $this->current_entity->output()[WorkModel::ID->value]) { - $this->updated_entity[WorkModel::ID->value] = $slug; + // Bail out if the slug generated from the new tite already exist + if ($this->get_entity_by_id($slug)) { + return new Response("An entity with this title already exist", 409); } + + // Add the new slug to update entity + $entity[WorkModel::ID] = $slug; } - // Update fractured dates from timestamp - $this->timestamp_to_dates(); - - // Attempt to update the entity - $update = $this->db->for(WorkModel::TABLE) - ->where([WorkModel::ID->value => $_GET["id"]]) - ->update($this->updated_entity); - - // Bail out if update failed - if (!$update) { - return $this->resp_database_error(); - } - - // Create new slug for entity if title was changed - if (!$this->change_slug()) { - return $this->resp_permalink_error_rollback(); + // Generate new work date fields from timestamp + if ($_POST[WorkModel::DATE_CREATED->value]) { + array_merge($entity, self::gen_date_created()); } - // Return 200 OK and new or existing entity slug as body - return new Response($this->current_entity->output()[WorkModel::ID->value]); + // Update entity by existing id + return $this->db->for(WorkModel::TABLE)->where([WorkModel::ID->value => $_GET[WorkModel::ID->value]])->update($entity) === true + ? new Response($_GET[WorkModel::ID->value]) + : new Response("Failed to update entity", 500); } } \ No newline at end of file diff --git a/api/endpoints/work/POST.php b/api/endpoints/work/POST.php index 90d0f0c..143cc4e 100755 --- a/api/endpoints/work/POST.php +++ b/api/endpoints/work/POST.php @@ -1,27 +1,28 @@ ruleset = new Ruleset(strict: true); $this->ruleset->POST([ @@ -37,12 +38,22 @@ ->max(parent::MYSQL_TEXT_MAX_LENGTH) ->default(null), - (new Rules(WorkModel::DATE_TIMESTAMP_CREATED->value)) + (new Rules(WorkModel::IS_LISTABLE->value)) + ->type(Type::BOOLEAN) + ->default(false), + + (new Rules(WorkModel::IS_READABLE->value)) + ->type(Type::BOOLEAN) + ->default(false), + + (new Rules(WorkModel::DATE_CREATED->value)) ->type(Type::NUMBER) ->min(1) ->max(parent::MYSQL_INT_MAX_LENGHT) - ->default(null) + ->default(time()) ]); + + parent::__construct($this->ruleset); } // Generate a slug URL from string @@ -50,84 +61,51 @@ return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input))); } - // Create permalink for entity slug - private function create_permalink(string $slug): bool { - $create = Call("work/permalinks", Method::POST, [ - WorkPermalinksModel::SLUG->value => $slug, - WorkPermalinksModel::ANCHOR->value => $slug - ]); + // Compute and return modeled year, month, and day from a Unix timestamp + private static function gen_date_created(): array { + // Use provided timestamp in request + $date_created = $_POST[WorkModel::DATE_CREATED->value]; - return $create->ok; + return [ + WorkModel::DATE_YEAR->value => date("Y", $date_created), + WorkModel::DATE_MONTH ->value => date("n", $date_created), + WorkModel::DATE_DAY->value => date("j", $date_created) + ]; } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); + private function get_entity_by_id(string $id): Response { + return (new Call(Endpoints::WORK->value))->params([ + WorkModel::ID->value => $id + ])->get(); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } + // Use copy of request body as entity + $entity = $_POST; // Generate URL slug from title text or UUID if undefined - $slug = !empty($_POST["title"]) ? self::gen_slug($_POST["title"]) : parent::gen_uuid4(); + $entity[WorkModel::ID->value] = $_POST[WorkModel::TITLE->value] + ? self::gen_slug($_POST[WorkModel::TITLE->value]) + : parent::gen_uuid4(); - // Check if an entity already exists with slugified title from GET endpoint - $existing_entity = Call("work?id={$slug}", Method::GET); - // Response is not 404 (Not found) so we can't create the entity - if ($existing_entity->code !== 404) { - // Response is not a valid entity, something went wrong - if ($existing_entity->code !== 200) { - return $this->resp_database_error(); - } - - // Return 402 Conflict - return new Response("Entity with id '{$slug}' already exists", 402); + // Bail out here if a work entry with id had been created already + if ($this->get_entity_by_id($entity[WorkModel::ID->value])->ok) { + return new Response("An entity with id '{$slug}' already exist", 409); } - // Get created timestamp from payload or use current time if not specified - $created_timestamp = $_POST[WorkModel::DATE_TIMESTAMP_CREATED->value] - ? $_POST[WorkModel::DATE_TIMESTAMP_CREATED->value] - : time(); + // Generate the necessary date fields + array_merge($entity, self::gen_date_created()); - // Attempt to create new entity - $insert = $this->db->for(WorkModel::TABLE) - ->insert([ - WorkModel::ID->value => $slug, - WorkModel::TITLE->value => $_POST["title"], - WorkModel::SUMMARY->value => $_POST["summary"], - WorkModel::IS_LISTABLE->value => true, - WorkModel::IS_READABLE->value => true, - WorkModel::DATE_YEAR->value => date("Y", $created_timestamp), - WorkModel::DATE_MONTH ->value => date("n", $created_timestamp), - WorkModel::DATE_DAY->value => date("j", $created_timestamp), - WorkModel::DATE_TIMESTAMP_MODIFIED->value => null, - WorkModel::DATE_TIMESTAMP_CREATED->value => $created_timestamp, - ]); - - // Bail out if insert failed - if (!$insert) { - return $this->resp_database_error(); + // Let's try to insert the new entity + if (!$this->db->for(WorkModel::TABLE)->insert($entity)) { + return new Response("Failed to insert work entry", 500); } - // Create permalink for new entity - if (!$this->create_permalink($slug)) { - // Rollback created entity if permalink creation failed - Call("work", Method::DELETE, [WorkModel::ID->value => $slug]); - - return new Response("Failed to create permalink", 500); - } - - // Return 201 Created and entity slug as body - return new Response($slug, 201); + // Generate permalink for new entity + return (new Call(Endpoints::WORK_PERMALINKS->value))->post([ + WorkPermalinksModel::ID => $entity[WorkModel::ID->value], + WorkPermalinksModel::REF_WORK_ID => $entity[WorkModel::ID->value], + WorkPermalinksModel::DATE_CREATED => time() + ]); } } \ No newline at end of file diff --git a/api/endpoints/work/actions/DELETE.php b/api/endpoints/work/actions/DELETE.php index 2eb1e82..b64194b 100755 --- a/api/endpoints/work/actions/DELETE.php +++ b/api/endpoints/work/actions/DELETE.php @@ -6,9 +6,7 @@ use ReflectRules\Rules; use ReflectRules\Ruleset; - use Reflect\Method; - use function Reflect\Call; - + use const VLW\API\RESP_DELETE_OK; use VLW\API\Databases\VLWdb\VLWdb; use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel; @@ -23,51 +21,15 @@ $this->ruleset = new Ruleset(strict: true); $this->ruleset->POST([ - (new Rules("id")) - ->required() - ->type(Type::STRING) + (new Rules(WorkActionsModel::REF_WORK_ID->value)) ->min(1) ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) ]); } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); - } - public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - // Ensure the action exists by id - $existing_action = $this->db->for(WorkActionsModel::TABLE) - ->where([ - WorkActionsModel::ID->value => $_POST["id"] - ]) - ->select(null); - - // Return idempotent deletion if the action does not exist - if ($existing_action->num_rows === 0) { - return new Response($_POST["id"]); - } - - // Attempt to delete action by id - $delete = $this->db->for(WorkActionsModel::TABLE) - ->delete([ - WorkActionsModel::ID->value => $_POST["id"] - ]); - - // Return 201 Created and entity id as body if insert was successful - return $delete === true ? new Response($_POST["id"], 201) : $this->resp_database_error(); + return $this->db->for(WorkActionsModel::TABLE)->delete($_POST) === true + ? new Response(RESP_DELETE_OK) + : new Response("Failed to delete action for work entity", 500); } } \ No newline at end of file diff --git a/api/endpoints/work/actions/GET.php b/api/endpoints/work/actions/GET.php index 616dff9..ccbdb69 100644 --- a/api/endpoints/work/actions/GET.php +++ b/api/endpoints/work/actions/GET.php @@ -10,58 +10,37 @@ use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel; require_once Path::root("src/databases/VLWdb.php"); - require_once Path::root("src/databases/models/WorkActions.php"); + require_once Path::root("src/databases/models/Work/WorkActions.php"); class GET_WorkActions extends VLWdb { protected Ruleset $ruleset; public function __construct() { - parent::__construct(); $this->ruleset = new Ruleset(strict: true); $this->ruleset->GET([ - (new Rules(WorkActionsModel::ANCHOR->value)) - ->required() + (new Rules(WorkActionsModel::REF_WORK_ID->value)) ->type(Type::STRING) ->min(1) ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) ]); - } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); + parent::__construct($this->ruleset); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - $resp = $this->db->for(WorkActionsModel::TABLE) - ->where([WorkActionsModel::ANCHOR->value => $_GET[WorkActionsModel::ANCHOR->value]]) + $response = $this->db->for(WorkActionsModel::TABLE) + ->where($_GET) ->select([ + WorkActionsModel::REF_WORK_ID->value, WorkActionsModel::DISPLAY_TEXT->value, WorkActionsModel::HREF->value, WorkActionsModel::CLASS_LIST->value, WorkActionsModel::EXTERNAL->value ]); - // Bail out if something went wrong retrieving rows from the database - if (!parent::is_mysqli_result($resp)) { - return $this->resp_database_error(); - } - - return $resp->num_rows > 0 - ? new Response($resp->fetch_all(MYSQLI_ASSOC)) - : new Response([]); + return $response->num_rows > 0 + ? new Response($response->fetch_all(MYSQLI_ASSOC)) + : new Response([], 404); } } \ No newline at end of file diff --git a/api/endpoints/work/actions/POST.php b/api/endpoints/work/actions/POST.php index acca855..5587ddc 100755 --- a/api/endpoints/work/actions/POST.php +++ b/api/endpoints/work/actions/POST.php @@ -1,36 +1,36 @@ ruleset = new Ruleset(strict: true); - $this->ruleset->GET([ - (new Rules("id")) - ->required() - ->type(Type::STRING) - ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ]); - $this->ruleset->POST([ + (new Rules(WorkActionsModel::REF_WORK_ID->value)) + ->required() + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + (new Rules(WorkActionsModel::DISPLAY_TEXT->value)) ->required() ->type(Type::STRING) @@ -47,56 +47,31 @@ (new Rules(WorkActionsModel::CLASS_LIST->value)) ->type(Type::ARRAY) ->min(1) - ->max(4) ->default([]), (new Rules(WorkActionsModel::EXTERNAL->value)) ->type(Type::BOOLEAN) ->default(false) ]); + + parent::__construct($this->ruleset); } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); + private static function get_entity(): Response { + return (new Call(Endpoints::WORK->value))->params([ + WorkModel::ID->value => $_POST[WorkActionsModel::REF_WORK_ID->value] + ])->get(); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); + // Bail out if work entity could not be fetched + $entity = self::get_entity(); + if (!$entity->ok) { + return $entity; } - // Ensure an entity with the provided id exists - $entity = Call("work?id={$_GET["id"]}", Method::GET); - if ($entity->code !== 200) { - // Response from endpoint is not 404, something went wrong - if ($entity->code !== 404) { - return $this->resp_database_error(); - } - - return new Response("No entity with id '{$_GET["id"]}' was found", 404); - } - - // Attempt to create action for entity - $insert = $this->db->for(WorkActionsModel::TABLE) - ->insert([ - WorkActionsModel::ID->value => parent::gen_uuid4(), - WorkActionsModel::ANCHOR->value => $_GET["id"], - WorkActionsModel::DISPLAY_TEXT->value => $_POST[WorkActionsModel::DISPLAY_TEXT->value], - WorkActionsModel::HREF->value => $_POST[WorkActionsModel::HREF->value], - WorkActionsModel::CLASS_LIST->value => implode(",", $_POST[WorkActionsModel::CLASS_LIST->value]), - WorkActionsModel::EXTERNAL->value => $_POST[WorkActionsModel::EXTERNAL->value], - ]); - - // Return 201 Created and entity id as body if insert was successful - return $insert === true ? new Response($_GET["id"], 201) : $this->resp_database_error(); + return $this->db->for(WorkActionsModel::TABLE)->insert($_POST) === true + ? new Response($_POST[WorkActionsModel::REF_WORK_ID->value], 201) + : new Response("Failed to add action to work entity", 500); } } \ No newline at end of file diff --git a/api/endpoints/work/permalinks/GET.php b/api/endpoints/work/permalinks/GET.php index 9fe437b..4c35b6f 100755 --- a/api/endpoints/work/permalinks/GET.php +++ b/api/endpoints/work/permalinks/GET.php @@ -1,6 +1,5 @@ ruleset = new Ruleset(strict: true); $this->ruleset->GET([ - (new Rules("id")) - ->required() + (new Rules(WorkPermalinksModel::ID->value)) + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(WorkPermalinksModel::REF_WORK_ID->value)) ->type(Type::STRING) ->min(1) ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) ]); - } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to resolve permalink, please try again later", 503); + parent::__construct($this->ruleset); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } + $response = $this->db->for(WorkPermalinksModel::TABLE) + ->where($_GET) + ->select([ + WorkPermalinksModel::ID->value, + WorkPermalinksModel::REF_WORK_ID->value, + WorkPermalinksModel::DATE_CREATED->value + ]); - // Get all anchors that match the requested slug - $resolve = $this->db->for(WorkPermalinksModel::TABLE) - ->where([WorkPermalinksModel::SLUG->value => $_GET["id"]]) - ->select(WorkPermalinksModel::ANCHOR->value); - - // Return array of all matched work table ids. Or empty array if none found - return parent::is_mysqli_result($resolve) - ? new Response(array_column($resolve->fetch_all(MYSQLI_ASSOC), WorkPermalinksModel::ANCHOR->value)) - : $this->resp_database_error(); + return $response->num_rows > 0 + ? new Response($response->fetch_all(MYSQLI_ASSOC)) + : new Response([], 404); } } \ No newline at end of file diff --git a/api/endpoints/work/permalinks/POST.php b/api/endpoints/work/permalinks/POST.php index 6425316..2901217 100755 --- a/api/endpoints/work/permalinks/POST.php +++ b/api/endpoints/work/permalinks/POST.php @@ -1,83 +1,62 @@ ruleset = new Ruleset(strict: true); $this->ruleset->POST([ - (new Rules("slug")) + (new Rules(WorkPermalinksModel::ID->value)) ->required() ->type(Type::STRING) ->min(1) ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), - (new Rules("anchor")) + (new Rules(WorkPermalinksModel::REF_WORK_ID->value)) ->required() ->type(Type::STRING) ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(WorkPermalinksModel::DATE_CREATED->value)) + ->type(Type::NUMBER) + ->min(1) + ->max(parent::MYSQL_INT_MAX_LENGHT) + ->default(time()) ]); + + parent::__construct($this->ruleset); } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to resolve permalink, please try again later", 503); + private static function get_entity(): Response { + return (new Call(Endpoints::WORK->value))->params([ + WorkModel::ID->value => $_POST[WorkTagsModel::REF_WORK_ID->value] + ])->get(); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); + // Bail out if work entity could not be fetched + $entity = self::get_entity(); + if (!$entity->ok) { + return $entity; } - // Check if an entity exists with slug - $existing_entity = Call("work?id={$_POST["slug"]}", Method::GET); - // Response is not 404 (Not found) so we can't create the entity - if ($existing_entity->code !== 200) { - // Response is not a valid entity, something went wrong - if ($existing_entity->code !== 404) { - return $this->resp_database_error(); - } - - // Return 402 Conflict - return new Response("No work entity with id '{$_POST["slug"]}' was found to permalink", 404); - } - - // Attempt to create new entity - $insert = $this->db->for(WorkPermalinksModel::TABLE) - ->insert([ - WorkPermalinksModel::SLUG->value => $_POST["slug"], - WorkPermalinksModel::ANCHOR->value => $_POST["anchor"], - WorkPermalinksModel::DATE_TIMESTAMP_CREATED->value => time(), - ]); - - // Return 201 Created and entity slug as body if insert was successful - return $insert === true ? new Response($_POST["slug"], 201) : $this->resp_database_error(); + return $this->db->for(WorkPermalinksModel::TABLE)->insert($_POST) === true + ? new Response($_POST[WorkPermalinksModel::ID->value], 201) + : new Response("Failed to add permalink to work entity", 500); } } \ No newline at end of file diff --git a/api/endpoints/work/tags/DELETE.php b/api/endpoints/work/tags/DELETE.php index f674469..2699806 100755 --- a/api/endpoints/work/tags/DELETE.php +++ b/api/endpoints/work/tags/DELETE.php @@ -6,75 +6,34 @@ use ReflectRules\Rules; use ReflectRules\Ruleset; - use Reflect\Method; - use function Reflect\Call; - + use const VLW\API\RESP_DELETE_OK; use VLW\API\Databases\VLWdb\VLWdb; use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel; - use VLW\API\Databases\VLWdb\Models\Work\WorkTagsNameEnum; require_once Path::root("src/databases/VLWdb.php"); - require_once Path::root("src/databases/models/WorkTags.php"); + require_once Path::root("src/databases/models/Work/WorkTags.php"); class DELETE_WorkTags extends VLWdb { - protected Ruleset $ruleset; + private Ruleset $ruleset; public function __construct() { - parent::__construct(); $this->ruleset = new Ruleset(strict: true); - $this->ruleset->POST([ - (new Rules("id")) - ->required() - ->type(Type::STRING) + $this->ruleset->GET([ + (new Rules(WorkTagsModel::REF_WORK_ID->value)) ->min(1) ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), - + (new Rules(WorkTagsModel::NAME->value)) - ->required() ->type(Type::ENUM, WorkTagsNameEnum::names()) ]); - } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); + parent::__construct($this->ruleset); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); - } - - // Ensure the tag exists for entity id - $existing_tag = $this->db->for(WorkTagsModel::TABLE) - ->where([ - WorkTagsModel::ANCHOR->value => $_POST["id"], - WorkTagsModel::NAME->value => $_POST["name"] - ]) - ->select(null); - - // Return idempotent deletion if the tag does not exist - if ($existing_tag->num_rows === 0) { - return new Response($_POST["id"]); - } - - // Attempt to delete tag for entity - $delete = $this->db->for(WorkTagsModel::TABLE) - ->delete([ - WorkTagsModel::ANCHOR->value => $_POST["id"], - WorkTagsModel::NAME->value => $_POST["name"] - ]); - - // Return 201 Created and entity id as body if insert was successful - return $delete === true ? new Response($_POST["id"], 201) : $this->resp_database_error(); + return $this->db->for(WorkTagsModel::TABLE)->delete($_POST) === true + ? new Response(RESP_DELETE_OK) + : new Response("Failed to delete value from document", 500); } } \ No newline at end of file diff --git a/api/endpoints/work/tags/GET.php b/api/endpoints/work/tags/GET.php new file mode 100644 index 0000000..7b6841b --- /dev/null +++ b/api/endpoints/work/tags/GET.php @@ -0,0 +1,48 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules(WorkTagsModel::REF_WORK_ID->value)) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(WorkTagsModel::NAME->value)) + ->type(Type::ENUM, WorkTagsNameEnum::names()) + ]); + + parent::__construct($this->ruleset); + } + + public function main(): Response { + $response = $this->db->for(WorkTagsModel::TABLE) + ->where($_GET) + ->select([ + WorkTagsModel::REF_WORK_ID->value, + WorkTagsModel::NAME->value + ]); + + return $response->num_rows > 0 + ? new Response($response->fetch_all(MYSQLI_ASSOC)) + : new Response([], 404); + } + } \ No newline at end of file diff --git a/api/endpoints/work/tags/POST.php b/api/endpoints/work/tags/POST.php index d78c03c..a99205f 100755 --- a/api/endpoints/work/tags/POST.php +++ b/api/endpoints/work/tags/POST.php @@ -1,93 +1,60 @@ ruleset = new Ruleset(strict: true); - $this->ruleset->GET([ - (new Rules("id")) - ->required() - ->type(Type::STRING) - ->min(1) - ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) - ]); - $this->ruleset->POST([ + (new Rules(WorkTagsModel::REF_WORK_ID->value)) + ->required() + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + (new Rules(WorkTagsModel::NAME->value)) ->required() ->type(Type::ENUM, WorkTagsNameEnum::names()) ]); + + parent::__construct($this->ruleset); } - // # Responses - - // Return 422 Unprocessable Content error if request validation failed - private function resp_rules_invalid(): Response { - return new Response($this->ruleset->get_errors(), 422); - } - - // Return a 503 Service Unavailable error if something went wrong with the database call - private function resp_database_error(): Response { - return new Response("Failed to get work data, please try again later", 503); + private static function get_entity(): Response { + return (new Call(Endpoints::WORK->value))->params([ + WorkModel::ID->value => $_POST[WorkTagsModel::REF_WORK_ID->value] + ])->get(); } public function main(): Response { - // Bail out if request validation failed - if (!$this->ruleset->is_valid()) { - return $this->resp_rules_invalid(); + // Bail out if work entity could not be fetched + $entity = self::get_entity(); + if (!$entity->ok) { + return $entity; } - // Ensure an entity with the provided id exists - $entity = Call("work?id={$_GET["id"]}", Method::GET); - if ($entity->code !== 200) { - // Response from endpoint is not 404, something went wrong - if ($entity->code !== 404) { - return $this->resp_database_error(); - } - - return new Response("No entity with id '{$_GET["id"]}' was found", 404); - } - - // Ensure the tag does not already exist for entity - $existing_tag = $this->db->for(WorkTagsModel::TABLE) - ->where([ - WorkTagsModel::ANCHOR->value => $_GET["id"], - WorkTagsModel::NAME->value => $_POST["name"] - ]) - ->select(null); - - // Bail out if this tag already exists - if ($existing_tag->num_rows !== 0) { - return new Response("Tag '{$_POST["name"]}' is already set on entity id '{$_GET["id"]}'", 402); - } - - // Attempt to create tag for entity - $insert = $this->db->for(WorkTagsModel::TABLE) - ->insert([ - WorkTagsModel::ANCHOR->value => $_GET["id"], - WorkTagsModel::NAME->value => $_POST["name"] - ]); - - // Return 201 Created and entity id as body if insert was successful - return $insert === true ? new Response($_GET["id"], 201) : $this->resp_database_error(); + return $this->db->for(WorkTagsModel::TABLE)->insert($_POST) === true + ? new Response($_POST[WorkTagsModel::REF_WORK_ID->value], 201) + : new Response("Failed to add tag to work entity", 500); } } \ No newline at end of file diff --git a/api/src/Endpoints.php b/api/src/Endpoints.php new file mode 100644 index 0000000..cb6bb87 --- /dev/null +++ b/api/src/Endpoints.php @@ -0,0 +1,17 @@ +db = new MySQL( $_ENV["vlwdb"]["mariadb_host"], @@ -46,7 +55,8 @@ ); } - public static function is_mysqli_result(\mysqli_result|bool $resp): bool { - return $resp instanceof \mysqli_result; + // Bail out if provided ReflectRules\Ruleset is invalid + private static function eval_ruleset_or_exit(Ruleset $ruleset): ?Response { + return !$ruleset->is_valid() ? new Response($ruleset->get_errors(), 422) : null; } } \ No newline at end of file diff --git a/api/src/databases/models/Coffee.php b/api/src/databases/models/Coffee.php deleted file mode 100755 index 0d4377c..0000000 --- a/api/src/databases/models/Coffee.php +++ /dev/null @@ -1,10 +0,0 @@ - @@ -50,8 +54,8 @@ // Send message via API $send = $api->call(Endpoints::MESSAGES->value)->post([ - ContactFieldsEnum::EMAIL->value => $_POST[ContactFieldsEnum::EMAIL->value], - ContactFieldsEnum::MESSAGE->value => $_POST[ContactFieldsEnum::MESSAGE->value] + MessagesModel::EMAIL->value => $_POST[MessagesModel::EMAIL->value], + MessagesModel::MESSAGE->value => $_POST[MessagesModel::MESSAGE->value] ]); ?> @@ -74,11 +78,11 @@
- + - +
diff --git a/pages/search.php b/pages/search.php index 068d8a5..9e2ca7b 100755 --- a/pages/search.php +++ b/pages/search.php @@ -1,24 +1,36 @@ call(Endpoints::SEARCH->value) - // Get query string from search parameter if set - ->params(["q" => $query]) - ->get(); + $response = $api->call(Endpoints::SEARCH->value)->params([SEARCH_PARAM => $_GET[SEARCH_PARAM]])->get(); ?> - +ok): ?> - json(); ?> + json(); ?> - - code): default: ?> - -
-

Something went wrong

-
- + + value]): ?> +
+

Work

+

value]) ?> search result(s) from my public work

+
+
- - + + value] as $result): ?> +
+

value] ?>

+

value] ?>

+

value]) ?>

- - 0): ?> - - + + call(Endpoints::WORK_ACTIONS->value)->params([WorkActionsModel::REF_WORK_ID->value => $result[WorkModel::ID->value]])->get(); ?> - - -
-

Work

-

search result(s) from my public work

-
-
- - - -
-

-

-

- - - - + + ok): ?> +
+ json() as $action): ?> + + + value]): ?> + + + -
- - -
- - - - -
- -

No results for search term ""

-
- - - - - - -
-

Connection to VLW API was successful but lacking permission to search

-
- - - - - - - $error_msg): ?> - - - -
-

Unknown request validation error

-
- - - - -
- -

type at least characters to search!

-
- - - + +
+ + - - - - - - +
+ - + + +
+ +

No results for search term ""

+
+ +
+ +

Start typing to search

+
+ + \ No newline at end of file diff --git a/pages/work.php b/pages/work.php index c8c6930..5195839 100755 --- a/pages/work.php +++ b/pages/work.php @@ -1,13 +1,34 @@ call(Endpoints::WORK->value)->get(); + require_once Path::root("src/client/API.php"); + require_once Path::root("api/src/Endpoints.php"); + + require_once Path::root("api/src/databases/models/Work/Work.php"); + require_once Path::root("api/src/databases/models/Work/WorkTags.php"); + require_once Path::root("api/src/databases/models/Work/WorkActions.php"); + + // Connect to VLW API + $api = new API(); + + // Retreive rows from work endpoints + $resp_work = $api->call(Endpoints::WORK->value)->get(); + + // Resolve tags and actions if we got work results + if ($resp_work->ok) { + $work_tags = $api->call(Endpoints::WORK_TAGS->value)->get()->json(); + $work_actions = $api->call(Endpoints::WORK_ACTIONS->value)->get()->json(); + } ?> @@ -21,7 +42,7 @@ -ok): ?> +ok): ?> json() as $row) { + foreach ($resp_work->json() as $row) { // Create array for current year if it doesn't exist - if (!array_key_exists($row["date_year"], $rows)) { - $rows[$row["date_year"]] = []; + if (!array_key_exists($row[WorkModel::DATE_YEAR->value], $rows)) { + $rows[$row[WorkModel::DATE_YEAR->value]] = []; } // Create array for current month if it doesn't exist - if (!array_key_exists($row["date_month"], $rows[$row["date_year"]])) { - $rows[$row["date_year"]][$row["date_month"]] = []; + if (!array_key_exists($row[WorkModel::DATE_MONTH->value], $rows[$row[WorkModel::DATE_YEAR->value]])) { + $rows[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]] = []; } // Create array for current day if it doesn't exist - if (!array_key_exists($row["date_day"], $rows[$row["date_year"]][$row["date_month"]])) { - $rows[$row["date_year"]][$row["date_month"]][$row["date_day"]] = []; + if (!array_key_exists($row[WorkModel::DATE_DAY->value], $rows[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]])) { + $rows[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]][$row[WorkModel::DATE_DAY->value]] = []; } // Append item to ordered array - $rows[$row["date_year"]][$row["date_month"]][$row["date_day"]][] = $row; + $rows[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]][$row[WorkModel::DATE_DAY->value]][] = $row; } ?> @@ -84,52 +105,50 @@
- - + + value), $item[WorkModel::ID->value]); ?> + + +
- -

">

+ + + + +

value] ?>

- -

+ value])): ?> +

value] ?>

- - - +

value] ?>

- - - - - - ." type=""> - - - - ." type="" loading="lazy"/> -
- - -

+ + value), $item[WorkModel::ID->value]); ?> - +
- + value] + // Bind VV Interactions for local links + ? "vv='work' vv-call='navigate'" + // Open external links in a new tab + : "target='_blank'"; + + $link_href = $action[WorkActionsModel::HREF->value] === null + // Navigate to work details page if no href is defined + ? "/work/{$item[WorkModel::ID->value]}" + // Href is defined so use it directly + : $action[WorkActionsModel::HREF->value]; ?> > diff --git a/src/packages/API/src/Client.php b/src/client/API.php similarity index 70% rename from src/packages/API/src/Client.php rename to src/client/API.php index 2f5dd7d..bb314eb 100644 --- a/src/packages/API/src/Client.php +++ b/src/client/API.php @@ -1,10 +1,10 @@ id = $id; + $this->api = new Api(); + $this->endpoint = $endpoint; + + $this->resolve_entity_by_id(); + } + + private function resolve_entity_by_id() { + // Bail out wit a dummy Response if no id was provided + if (!$this->id) { + $this->response = new Response("", 404); + return; + } + + $this->response = $this->api + ->call($this->endpoint->value) + ->params([self::ENTITY_ID => $this->id]) + ->get(); + + // Load response into entity object if successful + if ($this->response->ok) { + $this->entity = (object) $this->response->json()[0]; + } + } + + public function resolve(Endpoints $endpoint, array $params): array { + $response = $this->api->call($endpoint->value)->params($params)->get(); + + return $response->ok ? $response->json() : []; + } + } \ No newline at end of file diff --git a/src/entities/VLW/Work.php b/src/entities/VLW/Work.php new file mode 100644 index 0000000..5b4b26a --- /dev/null +++ b/src/entities/VLW/Work.php @@ -0,0 +1,74 @@ +signatures) { + return $this->signatures; + } + + foreach ($this->resolve(Endpoints::ICELDB_ANALYSES_SIGNATURES, ["ref_analysis_id" => $this->id]) as $rel) { + $this->signatures[$rel["id"]] = $rel; + $this->signatures[$rel["id"]]["user"] = $this->resolve(Endpoints::ICELDB_USERS, ["ref_user_id" => $rel["ref_user_id"]])[0]; + } + + return $this->signatures; + } + + public function documents(): array { + if ($this->documents) { + return $this->documents; + } + + $this->documents = $this->resolve(Endpoints::ICELDB_ANALYSES_DOCUMENTS, ["ref_analysis_id" => $this->id]); + return $this->documents; + } + + public function studies(): array { + if ($this->studies) { + return $this->studies; + } + + foreach ($this->resolve(Endpoints::ICELDB_STUDIES_ANALYSES, ["ref_analysis_id" => $this->id]) as $rel) { + $this->studies[] = $this->resolve(Endpoints::ICELDB_STUDIES, ["id" => $rel["ref_study_id"]]); + } + + return $this->studies; + } + + public function notes(): array { + if ($this->notes) { + return $this->notes; + } + + $this->notes = $this->resolve(Endpoints::ICELDB_ANALYSES_NOTES, ["ref_analysis_id" => $this->id]); + return $this->notes; + } + + public function individuals(): array { + if ($this->individuals) { + return $this->individuals; + } + + foreach ($this->resolve(Endpoints::ICELDB_ANALYSES_INDIVIDUALS, ["ref_analysis_id" => $this->id]) as $rel) { + $this->individuals[] = $this->resolve(Endpoints::ICELDB_INDIVIDUALS, ["id" => $rel["ref_individual_id"]])[0]; + } + + return $this->individuals; + } + } \ No newline at end of file diff --git a/src/packages/API/composer.json b/src/packages/API/composer.json deleted file mode 100644 index 2edfca6..0000000 --- a/src/packages/API/composer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "local/api.client", - "description": "Wrapper for vlw.se API", - "type": "library", - "version": "1.0.0-dev", - "authors": [ - { - "name": "Victor Westerlund", - "email": "victor@vlw.se" - } - ], - "autoload": { - "psr-4": { - "VLW\\API\\": "src/" - } - }, - "require": { - "reflect/client": "^3.0" - } -} diff --git a/src/packages/API/composer.lock b/src/packages/API/composer.lock deleted file mode 100644 index 2896728..0000000 --- a/src/packages/API/composer.lock +++ /dev/null @@ -1,58 +0,0 @@ -{ - "_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": "ea73f1cfa968f06be9c2ac54d2a56c96", - "packages": [ - { - "name": "reflect/client", - "version": "dev-feat/responseobj", - "source": { - "type": "git", - "url": "https://github.com/VictorWesterlund/reflect-client-php.git", - "reference": "228b3c665d4af5023ac4ed71fe32bb144521b43b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/VictorWesterlund/reflect-client-php/zipball/228b3c665d4af5023ac4ed71fe32bb144521b43b", - "reference": "228b3c665d4af5023ac4ed71fe32bb144521b43b", - "shasum": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Reflect\\": "src/Reflect/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-only" - ], - "authors": [ - { - "name": "Victor Westerlund", - "email": "victor.vesterlund@gmail.com" - } - ], - "description": "Extendable PHP interface for communicating with Reflect API over HTTP or UNIX sockets", - "support": { - "issues": "https://github.com/VictorWesterlund/reflect-client-php/issues", - "source": "https://github.com/VictorWesterlund/reflect-client-php/tree/feat/responseobj" - }, - "time": "2024-03-18T11:57:57+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": { - "reflect/client": 20 - }, - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.0.0" -}