wip: 2024-06-13T07:32:13+0200 (1718256733)

This commit is contained in:
Victor Westerlund 2024-06-14 18:02:25 +02:00
parent fd04c3d5ae
commit 4b1fbf331c
33 changed files with 418 additions and 1454 deletions

View file

@ -1,39 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Coffee\CoffeeModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Coffee.php");
class GET_Coffee extends VLWdb {
const LIST_LIMIT = 20;
public function __construct() {
parent::__construct();
}
// 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 {
// Get the last LIST_LIMIT coffees from the database
$resp = $this->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();
}
}

View file

@ -1,36 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Coffee\CoffeeModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Coffee.php");
class POST_Coffee extends VLWdb {
public function __construct() {
parent::__construct();
}
// 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 record coffee! Ugh please take a note somewhere else", 503);
}
public function main(): Response {
// Generate UUID for entity
$id = parent::gen_uuid4();
// Attempt to create new entity
$insert = $this->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();
}
}

View file

@ -1,95 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Media\MediaModel;
use victorwesterlund\xEnum;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Media.php");
enum MediaDispositionEnum: string {
use xEnum;
case METADATA = "metadata";
case INLINE = "inline";
case DOWNLOAD = "download";
}
class GET_Media extends VLWdb {
const GET_DISPOSITION_KEY = "disposition";
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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;
}
}

View file

@ -1,117 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
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\Media\MediaModel;
use VLW\API\Databases\VLWdb\Models\Media\MediaTypeEnum;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Media.php");
class POST_Media extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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();
}
}

View file

@ -1,106 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Media\MediaModel;
use VLW\API\Databases\VLWdb\Models\MediaSrcset\MediaSrcsetModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Media.php");
require_once Path::root("src/databases/models/MediaSrcset.php");
class GET_MediaSrcset extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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)
]);
}
}

View file

@ -1,55 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\MediaSrcset\MediaSrcsetModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Media.php");
require_once Path::root("src/databases/models/MediaSrcset.php");
class POST_MediaSrcset extends VLWdb {
public function __construct() {
parent::__construct();
}
// # Responses
// 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 {
// Generate a random UUID for this srcset
$id = parent::gen_uuid4();
// Ensure an srcset with the generated id doesn't exist, although it shouldn't realistically ever happen
$srcset_existing = Call("media/srcset?id={$id}", Method::GET);
if ($srcset_existing->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();
}
}

View file

@ -6,14 +6,11 @@
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;
@ -36,44 +33,15 @@
]);
}
// # 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 {
//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($_POST) === true
? new Response($entity[MessagesModel::ID->value], 201)
: new Response("Failed to create message", 500);
}
}

View file

@ -1,223 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsNameEnum;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/databases/models/Work.php");
require_once Path::root("src/databases/models/WorkTags.php");
require_once Path::root("src/databases/models/WorkActions.php");
// "Virtual" database model for the POST request body since we're not writing to a db directly
enum ReleasesPostModel: string {
case GITHUB_USER = "user";
case GITHUB_REPO = "repo";
case GITHUB_TAG = "tag";
}
class POST_Releases {
// Base URL of the GitHub API (no tailing slash)
const GITHUB_API = "https://api.github.com";
const REGEX_HANDLE = "/@[\w]+/";
const REGEX_URL = "/\b(?:https?):\/\/\S+\b/";
protected Ruleset $ruleset;
protected CurlHandle $curl;
public function __construct() {
$this->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, "<a href='{$link[$i]}'>{$pr_id}</a>", $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, "<a href='{$url}'>{$handle[$i]}</a>", $line);
}
$output .= "<p>{$line}</p>";
}
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 = "<h3>Release {$title}@{$data["name"]}</h3>";
// 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);
}
}

View file

@ -1,15 +1,16 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use const VLW\API\RESP_DELETE_OK;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work.php");
@ -17,44 +18,47 @@
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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);
}
}

View file

@ -1,136 +1,76 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Method;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work.php");
require_once Path::root("src/databases/models/WorkTags.php");
require_once Path::root("src/databases/models/WorkActions.php");
class GET_Work extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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)
->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)
->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();
}
// Return details about a specific item by id
if (!empty($_GET["id"])) {
return $this->resp_item_details($_GET["id"]);
}
$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($_GET)
->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
WorkModel::IS_LISTABLE->value,
WorkModel::IS_READABLE->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);
}
}

View file

@ -1,30 +1,28 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkPermalinksModel
};
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/WorkPermalinks.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
class PATCH_Work extends VLWdb {
protected Ruleset $ruleset;
protected Response $current_entity;
protected array $updated_entity;
public function __construct() {
parent::__construct();
$this->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();
// Generate new work date fields from timestamp
if ($_POST[WorkModel::DATE_CREATED->value]) {
array_merge($entity, self::gen_date_created());
}
// Create new slug for entity if title was changed
if (!$this->change_slug()) {
return $this->resp_permalink_error_rollback();
}
// 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);
}
}

View file

@ -1,27 +1,28 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkPermalinksModel
};
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/WorkPermalinks.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
class POST_Work extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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()
]);
}
}

View file

@ -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);
}
}

View file

@ -16,52 +16,31 @@
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);
}
}

View file

@ -1,36 +1,36 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkActionsModel
};
require_once Path::root("src/Endpoints.php");
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/Work.php");
require_once Path::root("src/databases/models/Work/WorkActions.php");
class POST_WorkActions extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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);
}
}

View file

@ -1,6 +1,5 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
@ -8,53 +7,43 @@
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkPermalinksModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/WorkPermalinks.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
class GET_WorkPermalinks extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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);
}
}

View file

@ -1,83 +1,62 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
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\WorkPermalinks\WorkPermalinksModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkPermalinksModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/WorkPermalinks.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
class POST_WorkPermalinks extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,45 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkTags.php");
class GET_WorkTags extends VLWdb {
private Ruleset $ruleset;
public function __construct() {
$this->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(FieldsEnumsModel::TABLE)
->where($_GET)
->select([
FieldsEnumsModel::REF_WORK_ID->value,
FieldsEnumsModel::NAME->value
]);
return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
}
}

View file

@ -1,93 +1,60 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsNameEnum;
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkTagsModel,
WorkTagsNameEnum
};
require_once Path::root("src/Endpoints.php");
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/Work.php");
require_once Path::root("src/databases/models/Work/WorkTags.php");
class POST_WorkTags extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->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);
}
}

11
api/src/Endpoints.php Normal file
View file

@ -0,0 +1,11 @@
<?php
namespace VLW\API;
// Default string to return when a DELETE request is successful
const RESP_DELETE_OK = "OK";
// Enum of all available VLW endpoints grouped by category
enum Endpoints: string {
case WORK = "/work";
}

View file

@ -2,18 +2,27 @@
namespace VLW\API\Databases\VLWdb;
use Reflect\ENV;
use Reflect\Path;
use Reflect\Request;
use Reflect\Response;
use ReflectRules\Ruleset;
use libmysqldriver\MySQL;
class VLWdb {
const UUID_LENGTH = 36;
const MYSQL_TEXT_MAX_LENGTH = 65538;
const MYSQL_TEXT_MAX_LENGTH = 65538;
const MYSQL_VARCHAR_MAX_LENGTH = 255;
const MYSQL_INT_MAX_LENGHT = 2147483647;
const MYSQL_INT_MAX_LENGHT = 2147483647;
protected MySQL $db;
protected readonly MySQL $db;
public function __construct(Ruleset $ruleset) {
// Validate provided Ruleset before attempting to connect to the database
self::eval_ruleset_or_exit($ruleset);
public function __construct() {
// Create new MariaDB connection
$this->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;
}
}

View file

@ -1,10 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Coffee;
enum CoffeeModel: string {
const TABLE = "coffee";
case ID = "id";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

@ -1,24 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Media;
use victorwesterlund\xEnum;
enum MediaTypeEnum: string {
use xEnum;
case BLOB = "BLOB";
case IMAGE = "IMAGE";
}
enum MediaModel: string {
const TABLE = "media";
case ID = "id";
case NAME = "name";
case TYPE = "type";
case MIME = "mime";
case EXTENSION = "extension";
case SRCSET = "srcset";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

@ -1,10 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\MediaSrcset;
enum MediaSrcsetModel: string {
const TABLE = "media_srcset";
case ID = "id";
case ANCHOR_DEFAULT = "anchor_default";
}

View file

@ -1,19 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkModel: string {
const TABLE = "work";
case ID = "id";
case TITLE = "title";
case SUMMARY = "summary";
case COVER_SRCSET = "cover_srcset";
case IS_LISTABLE = "is_listable";
case IS_READABLE = "is_readable";
case DATE_YEAR = "date_year";
case DATE_MONTH = "date_month";
case DATE_DAY = "date_day";
case DATE_TIMESTAMP_MODIFIED = "date_timestamp_modified";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

@ -0,0 +1,19 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkModel: string {
const TABLE = "work";
case ID = "id";
case TITLE = "title";
case SUMMARY = "summary";
case COVER_SRCSET = "cover_srcset";
case IS_LISTABLE = "is_listable";
case IS_READABLE = "is_readable";
case DATE_YEAR = "date_year";
case DATE_MONTH = "date_month";
case DATE_DAY = "date_day";
case DATE_MODIFIED = "date_modified";
case DATE_CREATED = "date_created";
}

View file

@ -5,8 +5,7 @@
enum WorkActionsModel: string {
const TABLE = "work_actions";
case ID = "id";
case ANCHOR = "anchor";
case REF_WORK_ID = "ref_work_id";
case DISPLAY_TEXT = "display_text";
case HREF = "href";
case CLASS_LIST = "class_list";

View file

@ -1,6 +1,6 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\WorkPermalinks;
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkPermalinksModel: string {
const TABLE = "work_permalinks";

View file

@ -0,0 +1,20 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
use victorwesterlund\xEnum;
enum WorkTagsNameEnum {
use xEnum;
case VLW;
case RELEASE;
case WEBSITE;
}
enum WorkTagsModel: string {
const TABLE = "work_tags";
case REF_WORK_ID = "ref_work_id";
case NAME = "name";
}

View file

@ -1,20 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
use victorwesterlund\xEnum;
enum WorkTagsNameEnum: string {
use xEnum;
case VLW = "VLW";
case RELEASE = "RELEASE";
case WEBSITE = "WEBSITE";
}
enum WorkTagsModel: string {
const TABLE = "work_tags";
case ANCHOR = "anchor";
case NAME = "name";
}