From 99bfd7e505c7ac2ad0cbcd638ff31e194ccba33f Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Jan 2023 22:47:17 +0100 Subject: [PATCH] SQL expression rewrite, Pagination, some frontend stuff --- Core/API/GroupsAPI.class.php | 92 ++--- Core/API/Parameter/Parameter.class.php | 168 ++++----- Core/API/Parameter/StringType.class.php | 4 +- Core/API/Stats.class.php | 45 +-- Core/API/TemplateAPI.class.php | 2 +- Core/API/Traits/Pagination.trait.php | 109 ++++++ Core/API/UserAPI.class.php | 88 ++--- Core/API/VisitorsAPI.class.php | 3 +- Core/Documents/Install.class.php | 4 +- Core/Driver/SQL/Column/Column.class.php | 4 + Core/Driver/SQL/Condition/Compare.class.php | 14 + Core/Driver/SQL/Condition/CondAnd.class.php | 10 + Core/Driver/SQL/Condition/CondBool.class.php | 9 + Core/Driver/SQL/Condition/CondIn.class.php | 24 ++ .../SQL/Condition/CondKeyword.class.php | 9 + Core/Driver/SQL/Condition/CondLike.class.php | 1 + Core/Driver/SQL/Condition/CondNot.class.php | 14 +- Core/Driver/SQL/Condition/CondNull.class.php | 6 + Core/Driver/SQL/Condition/CondOr.class.php | 10 + Core/Driver/SQL/Condition/Exists.class.php | 17 +- Core/Driver/SQL/Expression/Alias.class.php | 32 ++ Core/Driver/SQL/Expression/CaseWhen.class.php | 10 + Core/Driver/SQL/Expression/Count.class.php | 24 ++ .../SQL/Expression/CurrentTimeStamp.class.php | 14 + Core/Driver/SQL/Expression/DateAdd.class.php | 28 ++ Core/Driver/SQL/Expression/DateSub.class.php | 28 ++ Core/Driver/SQL/Expression/Distinct.class.php | 22 ++ .../SQL/Expression/Expression.class.php | 4 + .../SQL/Expression/JsonArrayAgg.class.php | 26 +- Core/Driver/SQL/Expression/Sum.class.php | 16 +- Core/Driver/SQL/Keyword.class.php | 4 + Core/Driver/SQL/MySQL.class.php | 31 +- Core/Driver/SQL/PostgreSQL.class.php | 39 +-- Core/Driver/SQL/Query/Query.class.php | 4 + Core/Driver/SQL/Query/Select.class.php | 29 +- Core/Driver/SQL/SQL.class.php | 139 +------- Core/Driver/SQL/Type/CurrentColumn.php | 4 +- Core/Driver/SQL/Type/CurrentTable.class.php | 19 +- Core/Localization/de_DE/general.php | 3 + Core/Localization/en_US/general.php | 3 + .../Controller/DatabaseEntity.class.php | 37 +- .../Controller/DatabaseEntityHandler.php | 35 +- .../Controller/DatabaseEntityQuery.class.php | 29 +- .../Controller/NMRelation.class.php | 2 + Core/Objects/DatabaseEntity/Group.class.php | 10 + Core/core.php | 2 +- react/admin-panel/src/AdminDashboard.jsx | 24 +- react/admin-panel/src/App.jsx | 2 - react/admin-panel/src/elements/sidebar.js | 4 +- react/admin-panel/src/views/404.js | 5 + react/admin-panel/src/views/group-edit.js | 35 ++ react/admin-panel/src/views/group-list.js | 80 +++++ react/admin-panel/src/views/overview.js | 99 +++--- react/admin-panel/src/views/user-list.js | 90 +++++ react/package.json | 6 +- react/shared/api.js | 14 +- react/shared/elements/data-table.css | 20 ++ react/shared/elements/data-table.js | 323 ++++++++++++++++++ react/shared/hooks/pagination.js | 73 ++++ react/shared/locale.js | 50 +-- react/yarn.lock | 263 +++++++++++++- 61 files changed, 1745 insertions(+), 570 deletions(-) create mode 100644 Core/API/Traits/Pagination.trait.php create mode 100644 Core/Driver/SQL/Expression/Alias.class.php create mode 100644 Core/Driver/SQL/Expression/Count.class.php create mode 100644 Core/Driver/SQL/Expression/Distinct.class.php create mode 100644 react/admin-panel/src/views/404.js create mode 100644 react/admin-panel/src/views/group-edit.js create mode 100644 react/admin-panel/src/views/group-list.js create mode 100644 react/admin-panel/src/views/user-list.js create mode 100644 react/shared/elements/data-table.css create mode 100644 react/shared/elements/data-table.js create mode 100644 react/shared/hooks/pagination.js diff --git a/Core/API/GroupsAPI.class.php b/Core/API/GroupsAPI.class.php index 3df1d5d..66d6d87 100644 --- a/Core/API/GroupsAPI.class.php +++ b/Core/API/GroupsAPI.class.php @@ -2,6 +2,7 @@ namespace Core\API { + use Core\Driver\SQL\Expression\Count; use Core\Objects\Context; abstract class GroupsAPI extends Request { @@ -12,7 +13,7 @@ namespace Core\API { protected function groupExists($name): bool { $sql = $this->context->getSQL(); - $res = $sql->select($sql->count()) + $res = $sql->select(new Count()) ->from("Group") ->whereEq("name", $name) ->execute(); @@ -29,76 +30,81 @@ namespace Core\API\Groups { use Core\API\GroupsAPI; use Core\API\Parameter\Parameter; use Core\API\Parameter\StringType; + use Core\API\Traits\Pagination; + use Core\Driver\SQL\Column\Column; + use Core\Driver\SQL\Expression\Alias; + use Core\Driver\SQL\Expression\Count; use Core\Objects\Context; use Core\Objects\DatabaseEntity\Controller\NMRelation; use Core\Objects\DatabaseEntity\Group; class Fetch extends GroupsAPI { + use Pagination; + private int $groupCount; public function __construct(Context $context, $externalCall = false) { - parent::__construct($context, $externalCall, array( - 'page' => new Parameter('page', Parameter::TYPE_INT, true, 1), - 'count' => new Parameter('count', Parameter::TYPE_INT, true, 20) - )); + parent::__construct($context, $externalCall, + self::getPaginationParameters(['id', 'name', 'member_count']) + ); $this->groupCount = 0; } public function _execute(): bool { - $page = $this->getParam("page"); - if($page < 1) { - return $this->createError("Invalid page count"); - } - - $count = $this->getParam("count"); - if($count < 1 || $count > 50) { - return $this->createError("Invalid fetch count"); - } $sql = $this->context->getSQL(); - $groupCount = Group::count($sql); - if ($groupCount === false) { - return $this->createError("Error fetching group count: " . $sql->getLastError()); + if (!$this->initPagination($sql, Group::class)) { + return false; } - $groups = Group::findBy(Group::createBuilder($sql, false) - ->orderBy("id") - ->ascending() - ->limit($count) - ->offset(($page - 1) * $count)); + $memberCount = new Alias($sql->select(new Count()) + ->from(NMRelation::buildTableName("User", "Group")) + ->whereEq("group_id", new Column("Group.id")), "memberCount"); - if ($groups !== false) { + $groupsQuery = $this->createPaginationQuery($sql, [$memberCount]); + $groups = $groupsQuery->execute(); + if ($groups !== false && $groups !== null) { $this->result["groups"] = []; - $this->result["pageCount"] = intval(ceil($this->groupCount / $count)); - $this->result["totalCount"] = $this->groupCount; - foreach ($groups as $groupId => $group) { - $this->result["groups"][$groupId] = $group->jsonSerialize(); - $this->result["groups"][$groupId]["memberCount"] = 0; - } - - $nmTable = NMRelation::buildTableName("User", "Group"); - $res = $sql->select("group_id", $sql->count("user_id")) - ->from($nmTable) - ->groupBy("group_id") - ->execute(); - - if (is_array($res)) { - foreach ($res as $row) { - list ($groupId, $memberCount) = [$row["group_id"], $row["user_id_count"]]; - if (isset($this->result["groups"][$groupId])) { - $this->result["groups"][$groupId]["memberCount"] = $memberCount; - } - } + foreach ($groups as $group) { + $groupData = $group->jsonSerialize(); + $groupData["memberCount"] = $group["memberCount"]; + $this->result["groups"][] = $groupData; } + } else { + return $this->createError("Error fetching groups: " . $sql->getLastError()); } return $this->success; } } + class Get extends GroupsAPI { + public function __construct(Context $context, bool $externalCall = false) { + parent::__construct($context, $externalCall, [ + "id" => new Parameter("id", Parameter::TYPE_INT) + ]); + } + + protected function _execute(): bool { + $sql = $this->context->getSQL(); + $groupId = $this->getParam("id"); + $group = Group::find($sql, $groupId); + if ($group === false) { + return $this->createError("Error fetching group: " . $sql->getLastError()); + } else if ($group === null) { + return $this->createError("Group not found"); + } else { + $this->result["group"] = $group->jsonSerialize(); + $this->result["group"]["members"] = $group->getMembers($sql); + } + + return true; + } + } + class Create extends GroupsAPI { public function __construct(Context $context, $externalCall = false) { parent::__construct($context, $externalCall, array( diff --git a/Core/API/Parameter/Parameter.class.php b/Core/API/Parameter/Parameter.class.php index a4ba667..70843b6 100644 --- a/Core/API/Parameter/Parameter.class.php +++ b/Core/API/Parameter/Parameter.class.php @@ -5,22 +5,22 @@ namespace Core\API\Parameter; use DateTime; class Parameter { - const TYPE_INT = 0; - const TYPE_FLOAT = 1; - const TYPE_BOOLEAN = 2; - const TYPE_STRING = 3; - const TYPE_DATE = 4; - const TYPE_TIME = 5; + const TYPE_INT = 0; + const TYPE_FLOAT = 1; + const TYPE_BOOLEAN = 2; + const TYPE_STRING = 3; + const TYPE_DATE = 4; + const TYPE_TIME = 5; const TYPE_DATE_TIME = 6; - const TYPE_EMAIL = 7; + const TYPE_EMAIL = 7; // only internal access - const TYPE_RAW = 8; + const TYPE_RAW = 8; // only json will work here I guess // nope. also name[]=value - const TYPE_ARRAY = 9; - const TYPE_MIXED = 10; + const TYPE_ARRAY = 9; + const TYPE_MIXED = 10; const names = array('Integer', 'Float', 'Boolean', 'String', 'Date', 'Time', 'DateTime', 'E-Mail', 'Raw', 'Array', 'Mixed'); @@ -35,13 +35,15 @@ class Parameter { public bool $optional; public int $type; public string $typeName; + public ?array $choices; - public function __construct(string $name, int $type, bool $optional = FALSE, $defaultValue = NULL) { + public function __construct(string $name, int $type, bool $optional = FALSE, $defaultValue = NULL, ?array $choices = NULL) { $this->name = $name; $this->optional = $optional; $this->defaultValue = $defaultValue; $this->value = $defaultValue; $this->type = $type; + $this->choices = $choices; $this->typeName = $this->getTypeName(); } @@ -63,22 +65,22 @@ class Parameter { } public function getSwaggerFormat(): ?string { - switch ($this->type) { - case self::TYPE_DATE: - return self::DATE_FORMAT; - case self::TYPE_TIME: - return self::TIME_FORMAT; - case self::TYPE_DATE_TIME: - return self::DATE_TIME_FORMAT; - case self::TYPE_EMAIL: - return "email"; - default: - return null; - } + return match ($this->type) { + self::TYPE_DATE => self::DATE_FORMAT, + self::TYPE_TIME => self::TIME_FORMAT, + self::TYPE_DATE_TIME => self::DATE_TIME_FORMAT, + self::TYPE_EMAIL => "email", + default => null, + }; } public function getTypeName(): string { - return ($this->type >= 0 && $this->type < count(Parameter::names)) ? Parameter::names[$this->type] : "INVALID"; + $typeName = Parameter::names[$this->type] ?? "INVALID"; + if ($this->choices) { + $typeName .= ", choices: " . json_encode($this->choices); + } + + return $typeName; } public function toString(): string { @@ -86,7 +88,7 @@ class Parameter { $str = "$typeName $this->name"; $defaultValue = (is_null($this->value) ? 'NULL' : $this->value); - if($this->optional) { + if ($this->optional) { $str = "[$str = $defaultValue]"; } @@ -94,21 +96,21 @@ class Parameter { } public static function parseType($value): int { - if(is_array($value)) + if (is_array($value)) return Parameter::TYPE_ARRAY; - else if(is_numeric($value) && intval($value) == $value) + else if (is_numeric($value) && intval($value) == $value) return Parameter::TYPE_INT; - else if(is_float($value) || (is_numeric($value) && floatval($value) == $value)) + else if (is_float($value) || (is_numeric($value) && floatval($value) == $value)) return Parameter::TYPE_FLOAT; - else if(is_bool($value) || $value == "true" || $value == "false") + else if (is_bool($value) || $value == "true" || $value == "false") return Parameter::TYPE_BOOLEAN; - else if(is_a($value, 'DateTime')) + else if (is_a($value, 'DateTime')) return Parameter::TYPE_DATE_TIME; - else if($value !== null && ($d = DateTime::createFromFormat(self::DATE_FORMAT, $value)) && $d->format(self::DATE_FORMAT) === $value) + else if ($value !== null && ($d = DateTime::createFromFormat(self::DATE_FORMAT, $value)) && $d->format(self::DATE_FORMAT) === $value) return Parameter::TYPE_DATE; - else if($value !== null && ($d = DateTime::createFromFormat(self::TIME_FORMAT, $value)) && $d->format(self::TIME_FORMAT) === $value) + else if ($value !== null && ($d = DateTime::createFromFormat(self::TIME_FORMAT, $value)) && $d->format(self::TIME_FORMAT) === $value) return Parameter::TYPE_TIME; - else if($value !== null && ($d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value)) && $d->format(self::DATE_TIME_FORMAT) === $value) + else if ($value !== null && ($d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value)) && $d->format(self::DATE_TIME_FORMAT) === $value) return Parameter::TYPE_DATE_TIME; else if (filter_var($value, FILTER_VALIDATE_EMAIL)) return Parameter::TYPE_EMAIL; @@ -117,88 +119,90 @@ class Parameter { } public function parseParam($value): bool { - switch($this->type) { + + $valid = false; + switch ($this->type) { case Parameter::TYPE_INT: - if(is_numeric($value) && intval($value) == $value) { + if (is_numeric($value) && intval($value) == $value) { $this->value = intval($value); - return true; + $valid = true; } - return false; + break; case Parameter::TYPE_FLOAT: - if(is_numeric($value) && (floatval($value) == $value || intval($value) == $value)) { + if (is_numeric($value) && (floatval($value) == $value || intval($value) == $value)) { $this->value = floatval($value); - return true; + $valid = true; } - return false; + break; case Parameter::TYPE_BOOLEAN: - if(strcasecmp($value, 'true') === 0) + if (strcasecmp($value, 'true') === 0) { $this->value = true; - else if(strcasecmp($value, 'false') === 0) + $valid = true; + } else if (strcasecmp($value, 'false') === 0) { $this->value = false; - else if(is_bool($value)) + $valid = true; + } else if (is_bool($value)) { $this->value = (bool)$value; - else - return false; - return true; - - case Parameter::TYPE_DATE: - if(is_a($value, "DateTime")) { - $this->value = $value; - return true; + $valid = true; } - - $d = DateTime::createFromFormat(self::DATE_FORMAT, $value); - if($d && $d->format(self::DATE_FORMAT) === $value) { - $this->value = $d; - return true; - } - return false; + break; case Parameter::TYPE_TIME: - if(is_a($value, "DateTime")) { - $this->value = $value; - return true; - } - - $d = DateTime::createFromFormat(self::TIME_FORMAT, $value); - if($d && $d->format(self::TIME_FORMAT) === $value) { - $this->value = $d; - return true; - } - return false; - + case Parameter::TYPE_DATE: case Parameter::TYPE_DATE_TIME: - if(is_a($value, 'DateTime')) { + if ($value instanceof DateTime) { $this->value = $value; - return true; + $valid = true; } else { - $d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value); - if($d && $d->format(self::DATE_TIME_FORMAT) === $value) { + $format = $this->getFormat(); + $d = DateTime::createFromFormat($format, $value); + if ($d && $d->format($format) === $value) { $this->value = $d; - return true; + $valid = true; } } - return false; + break; case Parameter::TYPE_EMAIL: if (filter_var($value, FILTER_VALIDATE_EMAIL)) { $this->value = $value; - return true; + $valid = true; } - return false; + break; case Parameter::TYPE_ARRAY: - if(is_array($value)) { + if (is_array($value)) { $this->value = $value; - return true; + $valid = true; } - return false; + break; default: $this->value = $value; - return true; + $valid = true; + break; + } + + if ($valid && $this->choices) { + if (!in_array($this->value, $this->choices)) { + return false; + } + } + + return $valid; + } + + private function getFormat(): ?string { + if ($this->type === self::TYPE_TIME) { + return self::TIME_FORMAT; + } else if ($this->type === self::TYPE_DATE) { + return self::DATE_FORMAT; + } else if ($this->type === self::TYPE_DATE_TIME) { + return self::DATE_TIME_FORMAT; + } else { + return null; } } } \ No newline at end of file diff --git a/Core/API/Parameter/StringType.class.php b/Core/API/Parameter/StringType.class.php index 2d4188a..1916910 100644 --- a/Core/API/Parameter/StringType.class.php +++ b/Core/API/Parameter/StringType.class.php @@ -5,9 +5,9 @@ namespace Core\API\Parameter; class StringType extends Parameter { public int $maxLength; - public function __construct(string $name, int $maxLength = -1, bool $optional = FALSE, ?string $defaultValue = NULL) { + public function __construct(string $name, int $maxLength = -1, bool $optional = FALSE, ?string $defaultValue = NULL, ?array $choices = NULL) { $this->maxLength = $maxLength; - parent::__construct($name, Parameter::TYPE_STRING, $optional, $defaultValue); + parent::__construct($name, Parameter::TYPE_STRING, $optional, $defaultValue, $choices); } public function parseParam($value): bool { diff --git a/Core/API/Stats.class.php b/Core/API/Stats.class.php index 73d60c4..2edef1a 100644 --- a/Core/API/Stats.class.php +++ b/Core/API/Stats.class.php @@ -2,6 +2,8 @@ namespace Core\API; +use Core\Driver\SQL\Expression\Count; +use Core\Driver\SQL\Expression\Distinct; use DateTime; use Core\Driver\SQL\Condition\Compare; use Core\Driver\SQL\Condition\CondBool; @@ -16,24 +18,24 @@ class Stats extends Request { parent::__construct($context, $externalCall, array()); } - private function getUserCount() { + private function getUserCount(): int { $sql = $this->context->getSQL(); - $res = $sql->select($sql->count())->from("User")->execute(); + $res = $sql->select(new Count())->from("User")->execute(); $this->success = $this->success && ($res !== FALSE); $this->lastError = $sql->getLastError(); - return ($this->success ? $res[0]["count"] : 0); + return ($this->success ? intval($res[0]["count"]) : 0); } - private function getPageCount() { + private function getPageCount(): int { $sql = $this->context->getSQL(); - $res = $sql->select($sql->count())->from("Route") + $res = $sql->select(new Count())->from("Route") ->where(new CondBool("active")) ->execute(); $this->success = $this->success && ($res !== FALSE); $this->lastError = $sql->getLastError(); - return ($this->success ? $res[0]["count"] : 0); + return ($this->success ? intval($res[0]["count"]) : 0); } private function checkSettings(): bool { @@ -55,7 +57,7 @@ class Stats extends Request { $date = new DateTime(); $monthStart = $date->format("Ym00"); $monthEnd = $date->modify("+1 month")->format("Ym00"); - $res = $sql->select($sql->count($sql->distinct("cookie"))) + $res = $sql->select(new Count(new Distinct("cookie"))) ->from("Visitor") ->where(new Compare("day", $monthStart, ">=")) ->where(new Compare("day", $monthEnd, "<")) @@ -92,19 +94,22 @@ class Stats extends Request { return false; } - $this->result["userCount"] = $userCount; - $this->result["pageCount"] = $pageCount; - $this->result["visitors"] = $visitorStatistics; - $this->result["visitorsTotal"] = $visitorCount; - $this->result["server"] = array( - "version" => WEBBASE_VERSION, - "server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown", - "memory_usage" => memory_get_usage(), - "load_avg" => $loadAvg, - "database" => $this->context->getSQL()->getStatus(), - "mail" => $this->mailConfigured, - "reCaptcha" => $this->recaptchaConfigured - ); + $this->result["data"] = [ + "userCount" => $userCount, + "pageCount" => $pageCount, + "visitors" => $visitorStatistics, + "visitorsTotal" => $visitorCount, + "server" => [ + "version" => WEBBASE_VERSION, + "server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown", + "memory_usage" => memory_get_usage(), + "load_avg" => $loadAvg, + "database" => $this->context->getSQL()->getStatus(), + "mail" => $this->mailConfigured, + "reCaptcha" => $this->recaptchaConfigured + ], + ]; + return $this->success; } diff --git a/Core/API/TemplateAPI.class.php b/Core/API/TemplateAPI.class.php index d82a075..8557c38 100644 --- a/Core/API/TemplateAPI.class.php +++ b/Core/API/TemplateAPI.class.php @@ -72,7 +72,7 @@ namespace Core\API\Template { try { $this->result["html"] = $twigEnvironment->render($templateFile, $parameters); - } catch (LoaderError | RuntimeError | SyntaxError $e) { + } catch (LoaderError | RuntimeError | SyntaxError | \RuntimeException $e) { return $this->createError("Error rendering twig template: " . $e->getMessage()); } diff --git a/Core/API/Traits/Pagination.trait.php b/Core/API/Traits/Pagination.trait.php new file mode 100644 index 0000000..e50f746 --- /dev/null +++ b/Core/API/Traits/Pagination.trait.php @@ -0,0 +1,109 @@ + new Parameter('page', Parameter::TYPE_INT, true, 1), + 'count' => new Parameter('count', Parameter::TYPE_INT, true, 20), + 'orderBy' => new StringType('orderBy', -1, true, "id", $orderColumns), + 'sortOrder' => new StringType('sortOrder', -1, true, 'asc', ['asc', 'desc']), + ]; + } + + function initPagination(SQL $sql, string $class, ?Condition $condition = null, int $maxPageSize = 100): bool { + $this->paginationClass = $class; + $this->paginationCondition = $condition; + if (!$this->validateParameters($maxPageSize)) { + return false; + } + + $this->entityCount = call_user_func("$this->paginationClass::count", $sql, $condition); + if ($this->entityCount === false) { + return $this->createError("Error fetching $this->paginationClass::count: " . $sql->getLastError()); + } + + $pageCount = intval(ceil($this->entityCount / $this->pageSize)); + $this->page = min($this->page, $pageCount); // number of pages changed due to pageSize / filter + + $this->result["pagination"] = [ + "current" => $this->page, + "pageSize" => $this->pageSize, + "pageCount" => $pageCount, + "total" => $this->entityCount + ]; + + return true; + } + + function validateParameters(int $maxCount = 100): bool { + $this->page = $this->getParam("page"); + if ($this->page < 1) { + return $this->createError("Invalid page count"); + } + + $this->pageSize = $this->getParam("count"); + if ($this->pageSize < 1 || $this->pageSize > $maxCount) { + return $this->createError("Invalid fetch count"); + } + + return true; + } + + function createPaginationQuery(SQL $sql, array $additionalValues = []): DatabaseEntityQuery { + $page = $this->getParam("page"); + $count = $this->getParam("count"); + $orderBy = $this->getParam("orderBy"); + $sortOrder = $this->getParam("sortOrder"); + + $baseQuery = call_user_func("$this->paginationClass::createBuilder", $sql, false); + $entityQuery = $baseQuery + ->fetchEntities() + ->limit($count) + ->offset(($page - 1) * $count); + + if ($this->paginationCondition) { + $entityQuery->where($this->paginationCondition); + } + + if (!empty($additionalValues)) { + foreach ($additionalValues as $additionalValue) { + $entityQuery->addCustomValue($additionalValue); + } + } + + if ($orderBy) { + $handler = $baseQuery->getHandler(); + $baseTable = $handler->getTableName(); + $sortColumn = DatabaseEntityHandler::getColumnName($orderBy); + $fullyQualifiedColumn = "$baseTable.$sortColumn"; + $selectedColumns = $baseQuery->getSelectValues(); + + if (in_array($sortColumn, $selectedColumns)) { + $entityQuery->orderBy($sortColumn); + } else if (in_array($fullyQualifiedColumn, $selectedColumns)) { + $entityQuery->orderBy($fullyQualifiedColumn); + } else { + $entityQuery->orderBy($orderBy); + } + } + + if ($sortOrder === "asc") { + $entityQuery->ascending(); + } else { + $entityQuery->descending(); + } + + return $entityQuery; + } +} \ No newline at end of file diff --git a/Core/API/UserAPI.class.php b/Core/API/UserAPI.class.php index abdc771..c5ba0ce 100644 --- a/Core/API/UserAPI.class.php +++ b/Core/API/UserAPI.class.php @@ -132,11 +132,12 @@ namespace Core\API\User { use Core\API\Parameter\Parameter; use Core\API\Parameter\StringType; use Core\API\Template\Render; + use Core\API\Traits\Pagination; use Core\API\UserAPI; use Core\API\VerifyCaptcha; use Core\Driver\SQL\Condition\CondBool; - use Core\Driver\SQL\Condition\CondNot; use Core\Driver\SQL\Condition\CondOr; + use Core\Driver\SQL\Expression\Alias; use Core\Objects\DatabaseEntity\Group; use Core\Objects\DatabaseEntity\UserToken; use Core\Driver\SQL\Column\Column; @@ -209,96 +210,65 @@ namespace Core\API\User { class Fetch extends UserAPI { + use Pagination; + public function __construct(Context $context, $externalCall = false) { - parent::__construct($context, $externalCall, array( - 'page' => new Parameter('page', Parameter::TYPE_INT, true, 1), - 'count' => new Parameter('count', Parameter::TYPE_INT, true, 20) - )); - } - - private function selectIds($page, $count): array|bool { - $sql = $this->context->getSQL(); - $res = $sql->select("User.id") - ->from("User") - ->limit($count) - ->offset(($page - 1) * $count) - ->orderBy("User.id") - ->ascending() - ->execute(); - - $this->success = ($res !== NULL); - $this->lastError = $sql->getLastError(); - - if ($this->success && is_array($res)) { - return array_map(function ($row) { - return intval($row["id"]); - }, $res); - } - - return false; + parent::__construct($context, $externalCall, + self::getPaginationParameters(['id', 'name', 'email', 'groups', 'registeredAt']) + ); } public function _execute(): bool { - $page = $this->getParam("page"); - if ($page < 1) { - return $this->createError("Invalid page count"); - } - - $count = $this->getParam("count"); - if ($count < 1 || $count > 50) { - return $this->createError("Invalid fetch count"); - } - - $condition = null; $currentUser = $this->context->getUser(); $fullInfo = ($currentUser->hasGroup(Group::ADMIN) || - $currentUser->hasGroup(Group::SUPPORT)); + $currentUser->hasGroup(Group::SUPPORT)); + + $orderBy = $this->getParam("orderBy"); + $publicAttributes = ["id", "name", "fullName", "profilePicture", "email"]; // TODO: , "groupNames"]; + + $condition = null; if (!$fullInfo) { $condition = new CondOr( new Compare("User.id", $currentUser->getId()), new CondBool("User.confirmed") ); + + if ($orderBy && !in_array($orderBy, $publicAttributes)) { + return $this->createError("Insufficient permissions for sorting by field '$orderBy'"); + } } $sql = $this->context->getSQL(); - $userCount = User::count($sql, $condition); - if ($userCount === false) { - return $this->createError("Error fetching user count: " . $sql->getLastError()); + if (!$this->initPagination($sql, User::class, $condition)) { + return false; } - $userQuery = User::createBuilder($sql, false) - ->orderBy("id") - ->ascending() - ->limit($count) - ->offset(($page - 1) * $count) - ->fetchEntities(); - - if ($condition) { - $userQuery->where($condition); - } + $groupNames = new Alias( + $sql->select(new JsonArrayAgg("name"))->from("Group") + ->leftJoin("NM_Group_User", "NM_Group_User.group_id", "Group.id") + ->whereEq("NM_Group_User.user_id", new Column("User.id")), + "groups" + ); + $userQuery = $this->createPaginationQuery($sql, [$groupNames]); $users = User::findBy($userQuery); - if ($users !== false) { + if ($users !== false && $users !== null) { $this->result["users"] = []; foreach ($users as $userId => $user) { $serialized = $user->jsonSerialize(); if (!$fullInfo && $userId !== $currentUser->getId()) { - $publicAttributes = ["id", "name", "fullName", "profilePicture", "email", "groups"]; foreach (array_keys($serialized) as $attr) { if (!in_array($attr, $publicAttributes)) { - unset($serialized[$attr]); + unset ($serialized[$attr]); } } } - $this->result["users"][$userId] = $serialized; + $this->result["users"][] = $serialized; } - - $this->result["pageCount"] = intval(ceil($userCount / $count)); - $this->result["totalCount"] = $userCount; } else { return $this->createError("Error fetching users: " . $sql->getLastError()); } diff --git a/Core/API/VisitorsAPI.class.php b/Core/API/VisitorsAPI.class.php index 2555397..e6ed0b2 100644 --- a/Core/API/VisitorsAPI.class.php +++ b/Core/API/VisitorsAPI.class.php @@ -16,6 +16,7 @@ namespace Core\API\Visitors { use Core\API\Parameter\Parameter; use Core\API\Parameter\StringType; use Core\API\VisitorsAPI; + use Core\Driver\SQL\Expression\Count; use DateTime; use Core\Driver\SQL\Condition\Compare; use Core\Driver\SQL\Expression\Add; @@ -82,7 +83,7 @@ namespace Core\API\Visitors { $type = $this->getParam("type"); $sql = $this->context->getSQL(); - $query = $sql->select($sql->count(), "day") + $query = $sql->select(new Count(), "day") ->from("Visitor") ->whereGt("count", 1) ->groupBy("day") diff --git a/Core/Documents/Install.class.php b/Core/Documents/Install.class.php index 8949ad8..8c7f2b3 100644 --- a/Core/Documents/Install.class.php +++ b/Core/Documents/Install.class.php @@ -19,6 +19,7 @@ namespace Documents\Install { use Core\Configuration\Configuration; use Core\Configuration\CreateDatabase; + use Core\Driver\SQL\Expression\Count; use Core\Driver\SQL\Query\Commit; use Core\Driver\SQL\Query\StartTransaction; use Core\Driver\SQL\SQL; @@ -196,8 +197,7 @@ namespace Documents\Install { return self::DATABASE_CONFIGURATION; } - $countKeyword = $sql->count(); - $res = $sql->select($countKeyword)->from("User")->execute(); + $res = $sql->select(new Count())->from("User")->execute(); if ($res === FALSE) { return self::DATABASE_CONFIGURATION; } else { diff --git a/Core/Driver/SQL/Column/Column.class.php b/Core/Driver/SQL/Column/Column.class.php index 02c7198..1bd6b30 100644 --- a/Core/Driver/SQL/Column/Column.class.php +++ b/Core/Driver/SQL/Column/Column.class.php @@ -3,6 +3,7 @@ namespace Core\Driver\SQL\Column; use Core\Driver\SQL\Expression\Expression; +use Core\Driver\SQL\SQL; class Column extends Expression { @@ -20,4 +21,7 @@ class Column extends Expression { public function notNull(): bool { return !$this->nullable; } public function getDefaultValue() { return $this->defaultValue; } + function getExpression(SQL $sql, array &$params): string { + return $sql->columnName($this->name); + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/Compare.class.php b/Core/Driver/SQL/Condition/Compare.class.php index 9be64fd..7db63ab 100644 --- a/Core/Driver/SQL/Condition/Compare.class.php +++ b/Core/Driver/SQL/Condition/Compare.class.php @@ -2,6 +2,8 @@ namespace Core\Driver\SQL\Condition; +use Core\Driver\SQL\SQL; + class Compare extends Condition { private string $operator; @@ -18,4 +20,16 @@ class Compare extends Condition { public function getValue() { return $this->value; } public function getOperator(): string { return $this->operator; } + function getExpression(SQL $sql, array &$params): string { + + if ($this->value === null) { + if ($this->operator === "=") { + return $sql->columnName($this->column) . " IS NULL"; + } else if ($this->operator === "!=") { + return $sql->columnName($this->column) . " IS NOT NULL"; + } + } + + return $sql->columnName($this->column) . $this->operator . $sql->addValue($this->value, $params); + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/CondAnd.class.php b/Core/Driver/SQL/Condition/CondAnd.class.php index 7683df8..fda070d 100644 --- a/Core/Driver/SQL/Condition/CondAnd.class.php +++ b/Core/Driver/SQL/Condition/CondAnd.class.php @@ -2,6 +2,8 @@ namespace Core\Driver\SQL\Condition; +use Core\Driver\SQL\SQL; + class CondAnd extends Condition { private array $conditions; @@ -11,4 +13,12 @@ class CondAnd extends Condition { } public function getConditions(): array { return $this->conditions; } + + function getExpression(SQL $sql, array &$params): string { + $conditions = array(); + foreach($this->getConditions() as $cond) { + $conditions[] = $sql->addValue($cond, $params); + } + return "(" . implode(" AND ", $conditions) . ")"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/CondBool.class.php b/Core/Driver/SQL/Condition/CondBool.class.php index 3b2ead0..2d3c624 100644 --- a/Core/Driver/SQL/Condition/CondBool.class.php +++ b/Core/Driver/SQL/Condition/CondBool.class.php @@ -2,6 +2,8 @@ namespace Core\Driver\SQL\Condition; +use Core\Driver\SQL\SQL; + class CondBool extends Condition { private $value; @@ -12,4 +14,11 @@ class CondBool extends Condition { public function getValue() { return $this->value; } + function getExpression(SQL $sql, array &$params): string { + if (is_string($this->value)) { + return $sql->columnName($this->value); + } else { + return $sql->addValue($this->value); + } + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/CondIn.class.php b/Core/Driver/SQL/Condition/CondIn.class.php index 2ccd769..1ef02e7 100644 --- a/Core/Driver/SQL/Condition/CondIn.class.php +++ b/Core/Driver/SQL/Condition/CondIn.class.php @@ -2,6 +2,9 @@ namespace Core\Driver\SQL\Condition; +use Core\Driver\SQL\Query\Select; +use Core\Driver\SQL\SQL; + class CondIn extends Condition { private $needle; @@ -14,4 +17,25 @@ class CondIn extends Condition { public function getNeedle() { return $this->needle; } public function getHaystack() { return $this->haystack; } + + function getExpression(SQL $sql, array &$params): string { + + $haystack = $this->getHaystack(); + if (is_array($haystack)) { + $values = array(); + foreach ($haystack as $value) { + $values[] = $sql->addValue($value, $params); + } + + $values = implode(",", $values); + $values = "($values)"; + } else if($haystack instanceof Select) { + $values = $haystack->build($params); + } else { + $sql->getLogger()->error("Unsupported in-expression value: " . get_class($haystack)); + return false; + } + + return $sql->addValue($this->needle, $params) . " IN $values"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/CondKeyword.class.php b/Core/Driver/SQL/Condition/CondKeyword.class.php index b639ec0..5a87de1 100644 --- a/Core/Driver/SQL/Condition/CondKeyword.class.php +++ b/Core/Driver/SQL/Condition/CondKeyword.class.php @@ -2,6 +2,8 @@ namespace Core\Driver\SQL\Condition; +use Core\Driver\SQL\SQL; + abstract class CondKeyword extends Condition { private $leftExpression; @@ -17,4 +19,11 @@ abstract class CondKeyword extends Condition { public function getLeftExp() { return $this->leftExpression; } public function getRightExp() { return $this->rightExpression; } public function getKeyword(): string { return $this->keyword; } + + function getExpression(SQL $sql, array &$params): string { + $keyword = $this->getKeyword(); + $left = $sql->addValue($this->getLeftExp(), $params); + $right = $sql->addValue($this->getRightExp(), $params); + return "$left $keyword $right"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/CondLike.class.php b/Core/Driver/SQL/Condition/CondLike.class.php index 686c7fb..d793684 100644 --- a/Core/Driver/SQL/Condition/CondLike.class.php +++ b/Core/Driver/SQL/Condition/CondLike.class.php @@ -7,4 +7,5 @@ class CondLike extends CondKeyword { public function __construct($leftExpression, $rightExpression) { parent::__construct("LIKE", $leftExpression, $rightExpression); } + } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/CondNot.class.php b/Core/Driver/SQL/Condition/CondNot.class.php index 6d85798..606a523 100644 --- a/Core/Driver/SQL/Condition/CondNot.class.php +++ b/Core/Driver/SQL/Condition/CondNot.class.php @@ -2,15 +2,21 @@ namespace Core\Driver\SQL\Condition; +use Core\Driver\SQL\SQL; + class CondNot extends Condition { - private $expression; // string or condition + private mixed $expression; // string or condition - public function __construct($expression) { + public function __construct(mixed $expression) { $this->expression = $expression; } - public function getExpression() { - return $this->expression; + public function getExpression(SQL $sql, array &$params): string { + if (is_string($this->expression)) { + return "NOT " . $sql->columnName($this->expression); + } else { + return "NOT " . $sql->addValue($this->expression, $params); + } } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/CondNull.class.php b/Core/Driver/SQL/Condition/CondNull.class.php index 0bfe768..42e6b29 100644 --- a/Core/Driver/SQL/Condition/CondNull.class.php +++ b/Core/Driver/SQL/Condition/CondNull.class.php @@ -2,6 +2,8 @@ namespace Core\Driver\SQL\Condition; +use Core\Driver\SQL\SQL; + class CondNull extends Condition { private string $column; @@ -11,4 +13,8 @@ class CondNull extends Condition { } public function getColumn(): string { return $this->column; } + + function getExpression(SQL $sql, array &$params): string { + return $sql->columnName($this->getColumn()) . " IS NULL"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/CondOr.class.php b/Core/Driver/SQL/Condition/CondOr.class.php index fa0f961..c3d3d27 100644 --- a/Core/Driver/SQL/Condition/CondOr.class.php +++ b/Core/Driver/SQL/Condition/CondOr.class.php @@ -2,6 +2,8 @@ namespace Core\Driver\SQL\Condition; +use Core\Driver\SQL\SQL; + class CondOr extends Condition { private array $conditions; @@ -11,4 +13,12 @@ class CondOr extends Condition { } public function getConditions(): array { return $this->conditions; } + + function getExpression(SQL $sql, array &$params): string { + $conditions = array(); + foreach($this->getConditions() as $cond) { + $conditions[] = $sql->addValue($cond, $params); + } + return "(" . implode(" OR ", $conditions) . ")"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Condition/Exists.class.php b/Core/Driver/SQL/Condition/Exists.class.php index cd0cabc..f940aab 100644 --- a/Core/Driver/SQL/Condition/Exists.class.php +++ b/Core/Driver/SQL/Condition/Exists.class.php @@ -1,22 +1,23 @@ subQuery = $subQuery; } - public function getSubQuery(): Select - { + public function getSubQuery(): Select { return $this->subQuery; } + + function getExpression(SQL $sql, array &$params): string { + return "EXISTS(" .$this->getSubQuery()->build($params) . ")"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/Alias.class.php b/Core/Driver/SQL/Expression/Alias.class.php new file mode 100644 index 0000000..dbc2d41 --- /dev/null +++ b/Core/Driver/SQL/Expression/Alias.class.php @@ -0,0 +1,32 @@ +value = $value; + $this->alias = $alias; + } + + public function getAlias(): string { + return $this->alias; + } + + public function getValue(): mixed { + return $this->value; + } + + protected function addValue(SQL $sql, array &$params): string { + return $sql->addValue($this->value, $params); + } + + public function getExpression(SQL $sql, array &$params): string { + return $this->addValue($sql, $params) . " AS " . $this->getAlias(); + } +} \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/CaseWhen.class.php b/Core/Driver/SQL/Expression/CaseWhen.class.php index 386046b..6126735 100644 --- a/Core/Driver/SQL/Expression/CaseWhen.class.php +++ b/Core/Driver/SQL/Expression/CaseWhen.class.php @@ -3,6 +3,7 @@ namespace Core\Driver\SQL\Expression; use Core\Driver\SQL\Condition\Condition; +use Core\Driver\SQL\SQL; class CaseWhen extends Expression { @@ -20,4 +21,13 @@ class CaseWhen extends Expression { public function getTrueCase() { return $this->trueCase; } public function getFalseCase() { return $this->falseCase; } + function getExpression(SQL $sql, array &$params): string { + $condition = $sql->buildCondition($this->getCondition(), $params); + + // psql requires constant values here + $trueCase = $sql->addValue($this->getTrueCase(), $params, true); + $falseCase = $sql->addValue($this->getFalseCase(), $params, true); + + return "CASE WHEN $condition THEN $trueCase ELSE $falseCase END"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/Count.class.php b/Core/Driver/SQL/Expression/Count.class.php new file mode 100644 index 0000000..836bc02 --- /dev/null +++ b/Core/Driver/SQL/Expression/Count.class.php @@ -0,0 +1,24 @@ +getValue(); + if (is_string($value)) { + if ($value === "*") { + return "COUNT(*)"; + } else { + return "COUNT(" . $sql->columnName($value) . ")"; + } + } else { + return "COUNT(" . $sql->addValue($value, $params) . ")"; + } + } +} \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/CurrentTimeStamp.class.php b/Core/Driver/SQL/Expression/CurrentTimeStamp.class.php index 36987ad..164af4c 100644 --- a/Core/Driver/SQL/Expression/CurrentTimeStamp.class.php +++ b/Core/Driver/SQL/Expression/CurrentTimeStamp.class.php @@ -2,6 +2,20 @@ namespace Core\Driver\SQL\Expression; +use Core\Driver\SQL\MySQL; +use Core\Driver\SQL\PostgreSQL; +use Core\Driver\SQL\SQL; +use Exception; + class CurrentTimeStamp extends Expression { + function getExpression(SQL $sql, array &$params): string { + if ($sql instanceof MySQL) { + return "NOW()"; + } else if ($sql instanceof PostgreSQL) { + return "CURRENT_TIMESTAMP"; + } else { + throw new Exception("CurrentTimeStamp Not implemented for driver type: " . get_class($sql)); + } + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/DateAdd.class.php b/Core/Driver/SQL/Expression/DateAdd.class.php index 421eb41..430c2ec 100644 --- a/Core/Driver/SQL/Expression/DateAdd.class.php +++ b/Core/Driver/SQL/Expression/DateAdd.class.php @@ -2,6 +2,12 @@ namespace Core\Driver\SQL\Expression; +use Core\Driver\SQL\Column\Column; +use Core\Driver\SQL\MySQL; +use Core\Driver\SQL\PostgreSQL; +use Core\Driver\SQL\SQL; +use Core\External\PHPMailer\Exception; + class DateAdd extends Expression { private Expression $lhs; @@ -18,4 +24,26 @@ class DateAdd extends Expression { public function getRHS(): Expression { return $this->rhs; } public function getUnit(): string { return $this->unit; } + function getExpression(SQL $sql, array &$params): string { + if ($sql instanceof MySQL) { + $lhs = $sql->addValue($this->getLHS(), $params); + $rhs = $sql->addValue($this->getRHS(), $params); + $unit = $this->getUnit(); + return "DATE_ADD($lhs, INTERVAL $rhs $unit)"; + } else if ($sql instanceof PostgreSQL) { + $lhs = $sql->addValue($this->getLHS(), $params); + $rhs = $sql->addValue($this->getRHS(), $params); + $unit = $this->getUnit(); + + if ($this->getRHS() instanceof Column) { + $rhs = "$rhs * INTERVAL '1 $unit'"; + } else { + $rhs = "$rhs $unit"; + } + + return "$lhs - $rhs"; + } else { + throw new Exception("DateAdd Not implemented for driver type: " . get_class($sql)); + } + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/DateSub.class.php b/Core/Driver/SQL/Expression/DateSub.class.php index 17d8482..d953234 100644 --- a/Core/Driver/SQL/Expression/DateSub.class.php +++ b/Core/Driver/SQL/Expression/DateSub.class.php @@ -2,6 +2,12 @@ namespace Core\Driver\SQL\Expression; +use Core\Driver\SQL\Column\Column; +use Core\Driver\SQL\MySQL; +use Core\Driver\SQL\PostgreSQL; +use Core\Driver\SQL\SQL; +use Core\External\PHPMailer\Exception; + class DateSub extends Expression { private Expression $lhs; @@ -18,4 +24,26 @@ class DateSub extends Expression { public function getRHS(): Expression { return $this->rhs; } public function getUnit(): string { return $this->unit; } + function getExpression(SQL $sql, array &$params): string { + if ($sql instanceof MySQL) { + $lhs = $sql->addValue($this->getLHS(), $params); + $rhs = $sql->addValue($this->getRHS(), $params); + $unit = $this->getUnit(); + return "DATE_SUB($lhs, INTERVAL $rhs $unit)"; + } else if ($sql instanceof PostgreSQL) { + $lhs = $sql->addValue($this->getLHS(), $params); + $rhs = $sql->addValue($this->getRHS(), $params); + $unit = $this->getUnit(); + + if ($this->getRHS() instanceof Column) { + $rhs = "$rhs * INTERVAL '1 $unit'"; + } else { + $rhs = "$rhs $unit"; + } + + return "$lhs - $rhs"; + } else { + throw new Exception("DateSub Not implemented for driver type: " . get_class($sql)); + } + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/Distinct.class.php b/Core/Driver/SQL/Expression/Distinct.class.php new file mode 100644 index 0000000..31fa827 --- /dev/null +++ b/Core/Driver/SQL/Expression/Distinct.class.php @@ -0,0 +1,22 @@ +value = $value; + } + + public function getValue(): mixed { + return $this->value; + } + + function getExpression(SQL $sql, array &$params): string { + return "DISTINCT(" . $sql->addValue($this->getValue(), $params) . ")"; + } +} \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/Expression.class.php b/Core/Driver/SQL/Expression/Expression.class.php index 4ec9cfb..4e9d6fd 100644 --- a/Core/Driver/SQL/Expression/Expression.class.php +++ b/Core/Driver/SQL/Expression/Expression.class.php @@ -2,6 +2,10 @@ namespace Core\Driver\SQL\Expression; +use Core\Driver\SQL\SQL; + abstract class Expression { + abstract function getExpression(SQL $sql, array &$params): string; + } \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/JsonArrayAgg.class.php b/Core/Driver/SQL/Expression/JsonArrayAgg.class.php index c15858e..483e5c5 100644 --- a/Core/Driver/SQL/Expression/JsonArrayAgg.class.php +++ b/Core/Driver/SQL/Expression/JsonArrayAgg.class.php @@ -2,17 +2,29 @@ namespace Core\Driver\SQL\Expression; +use Core\Driver\SQL\Column\Column; +use Core\Driver\SQL\MySQL; +use Core\Driver\SQL\PostgreSQL; +use Core\Driver\SQL\SQL; +use Exception; + class JsonArrayAgg extends Expression { - private $value; - private string $alias; + private mixed $value; - public function __construct($value, string $alias) { + public function __construct(mixed $value) { $this->value = $value; - $this->alias = $alias; } - public function getValue() { return $this->value; } - public function getAlias(): string { return $this->alias; } - + public function getExpression(SQL $sql, array &$params): string { + $value = is_string($this->value) ? new Column($this->value) : $this->value; + $value = $sql->addValue($value, $params); + if ($sql instanceof MySQL) { + return "JSON_ARRAYAGG($value)"; + } else if ($sql instanceof PostgreSQL) { + return "JSON_AGG($value)"; + } else { + throw new Exception("JsonArrayAgg not implemented for driver type: " . get_class($sql)); + } + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Expression/Sum.class.php b/Core/Driver/SQL/Expression/Sum.class.php index 38c03fe..beb6822 100644 --- a/Core/Driver/SQL/Expression/Sum.class.php +++ b/Core/Driver/SQL/Expression/Sum.class.php @@ -2,17 +2,15 @@ namespace Core\Driver\SQL\Expression; -class Sum extends Expression { +use Core\Driver\SQL\SQL; - private $value; - private string $alias; +class Sum extends Alias { - public function __construct($value, string $alias) { - $this->value = $value; - $this->alias = $alias; + public function __construct(mixed $value, string $alias) { + parent::__construct($value, $alias); } - public function getValue() { return $this->value; } - public function getAlias(): string { return $this->alias; } - + protected function addValue(SQL $sql, array &$params): string { + return "SUM(" . $sql->addValue($this->getValue(), $params) . ")"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Keyword.class.php b/Core/Driver/SQL/Keyword.class.php index dc139eb..bb7dd6f 100644 --- a/Core/Driver/SQL/Keyword.class.php +++ b/Core/Driver/SQL/Keyword.class.php @@ -4,6 +4,7 @@ namespace Core\Driver\SQL; use Core\Driver\SQL\Expression\Expression; +// Unsafe sql class Keyword extends Expression { private string $value; @@ -14,4 +15,7 @@ class Keyword extends Expression { public function getValue(): string { return $this->value; } + function getExpression(SQL $sql, array &$params): string { + return $this->value; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/MySQL.class.php b/Core/Driver/SQL/MySQL.class.php index e9c0f77..fbe8ff1 100644 --- a/Core/Driver/SQL/MySQL.class.php +++ b/Core/Driver/SQL/MySQL.class.php @@ -17,10 +17,7 @@ use Core\Driver\SQL\Column\JsonColumn; use Core\Driver\SQL\Expression\Add; use Core\Driver\SQL\Expression\CurrentTimeStamp; -use Core\Driver\SQL\Expression\DateAdd; -use Core\Driver\SQL\Expression\DateSub; use Core\Driver\SQL\Expression\Expression; -use Core\Driver\SQL\Expression\JsonArrayAgg; use Core\Driver\SQL\Query\CreateProcedure; use Core\Driver\SQL\Query\CreateTrigger; use Core\Driver\SQL\Query\Query; @@ -337,14 +334,8 @@ class MySQL extends SQL { } public function addValue($val, &$params = NULL, bool $unsafe = false) { - if ($val instanceof Keyword) { - return $val->getValue(); - } else if ($val instanceof CurrentColumn) { - return $val->getName(); - } else if ($val instanceof Column) { - return $this->columnName($val->getName()); - } else if ($val instanceof Expression) { - return $this->createExpression($val, $params); + if ($val instanceof Expression) { + return $val->getExpression($this, $params); } else { if ($unsafe) { return $this->getUnsafeValue($val); @@ -460,24 +451,6 @@ class MySQL extends SQL { return $query; } - - protected function createExpression(Expression $exp, array &$params): ?string { - if ($exp instanceof DateAdd || $exp instanceof DateSub) { - $lhs = $this->addValue($exp->getLHS(), $params); - $rhs = $this->addValue($exp->getRHS(), $params); - $unit = $exp->getUnit(); - $dateFunction = ($exp instanceof DateAdd ? "DATE_ADD" : "DATE_SUB"); - return "$dateFunction($lhs, INTERVAL $rhs $unit)"; - } else if ($exp instanceof CurrentTimeStamp) { - return "NOW()"; - } else if ($exp instanceof JsonArrayAgg) { - $value = $this->addValue($exp->getValue(), $params); - $alias = $this->columnName($exp->getAlias()); - return "JSON_ARRAYAGG($value) as $alias"; - } else { - return parent::createExpression($exp, $params); - } - } } class RowIteratorMySQL extends RowIterator { diff --git a/Core/Driver/SQL/PostgreSQL.class.php b/Core/Driver/SQL/PostgreSQL.class.php index da1b0c4..6d15ac5 100644 --- a/Core/Driver/SQL/PostgreSQL.class.php +++ b/Core/Driver/SQL/PostgreSQL.class.php @@ -17,10 +17,7 @@ use Core\Driver\SQL\Column\JsonColumn; use Core\Driver\SQL\Condition\CondRegex; use Core\Driver\SQL\Expression\Add; use Core\Driver\SQL\Expression\CurrentTimeStamp; -use Core\Driver\SQL\Expression\DateAdd; -use Core\Driver\SQL\Expression\DateSub; use Core\Driver\SQL\Expression\Expression; -use Core\Driver\SQL\Expression\JsonArrayAgg; use Core\Driver\SQL\Query\CreateProcedure; use Core\Driver\SQL\Query\CreateTrigger; use Core\Driver\SQL\Query\Insert; @@ -301,16 +298,13 @@ class PostgreSQL extends SQL { } public function addValue($val, &$params = NULL, bool $unsafe = false) { - if ($val instanceof Keyword) { - return $val->getValue(); - } else if ($val instanceof CurrentTable) { + // I don't remember we need this here? + /*if ($val instanceof CurrentTable) { return "TG_TABLE_NAME"; } else if ($val instanceof CurrentColumn) { return "NEW." . $this->columnName($val->getName()); - } else if ($val instanceof Column) { - return $this->columnName($val->getName()); - } else if ($val instanceof Expression) { - return $this->createExpression($val, $params); + } else */if ($val instanceof Expression) { + return $val->getExpression($this, $params); } else { if ($unsafe) { return $this->getUnsafeValue($val); @@ -450,31 +444,6 @@ class PostgreSQL extends SQL { return $query; } - - protected function createExpression(Expression $exp, array &$params): ?string { - if ($exp instanceof DateAdd || $exp instanceof DateSub) { - $lhs = $this->addValue($exp->getLHS(), $params); - $rhs = $this->addValue($exp->getRHS(), $params); - $unit = $exp->getUnit(); - - if ($exp->getRHS() instanceof Column) { - $rhs = "$rhs * INTERVAL '1 $unit'"; - } else { - $rhs = "$rhs $unit"; - } - - $operator = ($exp instanceof DateAdd ? "+" : "-"); - return "$lhs $operator $rhs"; - } else if ($exp instanceof CurrentTimeStamp) { - return "CURRENT_TIMESTAMP"; - } else if ($exp instanceof JsonArrayAgg) { - $value = $this->addValue($exp->getValue(), $params); - $alias = $this->columnName($exp->getAlias()); - return "JSON_AGG($value) as $alias"; - } else { - return parent::createExpression($exp, $params); - } - } } class RowIteratorPostgreSQL extends RowIterator { diff --git a/Core/Driver/SQL/Query/Query.class.php b/Core/Driver/SQL/Query/Query.class.php index 6a9fd82..c48fe1f 100644 --- a/Core/Driver/SQL/Query/Query.class.php +++ b/Core/Driver/SQL/Query/Query.class.php @@ -26,4 +26,8 @@ abstract class Query extends Expression { } public abstract function build(array &$params): ?string; + + public function getExpression(SQL $sql, array &$params): string { + return "(" . $this->build($params) . ")"; + } } \ No newline at end of file diff --git a/Core/Driver/SQL/Query/Select.class.php b/Core/Driver/SQL/Query/Select.class.php index 3800bc0..a98dfca 100644 --- a/Core/Driver/SQL/Query/Select.class.php +++ b/Core/Driver/SQL/Query/Select.class.php @@ -3,6 +3,7 @@ namespace Core\Driver\SQL\Query; use Core\Driver\SQL\Condition\CondOr; +use Core\Driver\SQL\Expression\Expression; use Core\Driver\SQL\Expression\JsonArrayAgg; use Core\Driver\SQL\Join\InnerJoin; use Core\Driver\SQL\Join\Join; @@ -38,8 +39,13 @@ class Select extends ConditionalQuery { $this->fetchType = SQL::FETCH_ALL; } - public function addColumn(string $columnName): Select { - $this->selectValues[] = $columnName; + public function select(...$selectValues): Select { + $this->selectValues = (!empty($selectValues) && is_array($selectValues[0])) ? $selectValues[0] : $selectValues; + return $this; + } + + public function addSelectValue(...$selectValues): Select { + $this->selectValues = array_merge($this->selectValues, $selectValues); return $this; } @@ -142,25 +148,6 @@ class Select extends ConditionalQuery { foreach ($this->selectValues as $value) { if (is_string($value)) { $selectValues[] = $this->sql->columnName($value); - } else if ($value instanceof Select) { - $subSelect = $value->build($params); - if (count($value->getSelectValues()) !== 1) { - $selectValues[] = "($subSelect)"; - } else { - $columnAlias = null; - $subSelectColumn = $value->getSelectValues()[0]; - if (is_string($subSelectColumn) && ($index = stripos($subSelectColumn, " as ")) !== FALSE) { - $columnAlias = substr($subSelectColumn, $index + 4); - } else if ($subSelectColumn instanceof JsonArrayAgg) { - $columnAlias = $subSelectColumn->getAlias(); - } - - if ($columnAlias) { - $selectValues[] = "($subSelect) as $columnAlias"; - } else { - $selectValues[] = "($subSelect)"; - } - } } else { $selectValues[] = $this->sql->addValue($value, $params); } diff --git a/Core/Driver/SQL/SQL.class.php b/Core/Driver/SQL/SQL.class.php index f89e4b4..aa27838 100644 --- a/Core/Driver/SQL/SQL.class.php +++ b/Core/Driver/SQL/SQL.class.php @@ -4,24 +4,12 @@ namespace Core\Driver\SQL; use Core\Driver\Logger\Logger; use Core\Driver\SQL\Column\Column; -use Core\Driver\SQL\Condition\Compare; -use Core\Driver\SQL\Condition\CondAnd; -use Core\Driver\SQL\Condition\CondBool; -use Core\Driver\SQL\Condition\CondIn; use Core\Driver\SQL\Condition\Condition; -use Core\Driver\SQL\Condition\CondKeyword; -use Core\Driver\SQL\Condition\CondNot; -use Core\Driver\Sql\Condition\CondNull; -use Core\Driver\SQL\Condition\CondOr; -use Core\Driver\SQL\Condition\Exists; use Core\Driver\SQL\Constraint\Constraint; use Core\Driver\SQL\Constraint\Unique; use Core\Driver\SQL\Constraint\PrimaryKey; use Core\Driver\SQL\Constraint\ForeignKey; -use Core\Driver\SQL\Expression\CaseWhen; use Core\Driver\SQL\Expression\CurrentTimeStamp; -use Core\Driver\SQL\Expression\Expression; -use Core\Driver\SQL\Expression\Sum; use Core\Driver\SQL\Query\AlterTable; use Core\Driver\SQL\Query\Commit; use Core\Driver\SQL\Query\CreateProcedure; @@ -236,6 +224,7 @@ abstract class SQL { public abstract function createTriggerBody(CreateTrigger $trigger, array $params = []): ?string; public abstract function getProcedureHead(CreateProcedure $procedure): ?string; public abstract function getColumnType(Column $column): ?string; + public abstract function getStatus(); public function getProcedureTail(): string { return ""; } public function getReturning(?string $columns): string { return ""; } @@ -247,6 +236,10 @@ abstract class SQL { return $statements; } + public function getLogger(): Logger { + return $this->logger; + } + protected function getUnsafeValue($value): ?string { if (is_string($value)) { return "'" . addslashes("$value") . "'"; // unsafe operation here... @@ -273,60 +266,15 @@ abstract class SQL { public function now(): CurrentTimeStamp { return new CurrentTimeStamp(); } public function currentTimestamp(): CurrentTimeStamp { return new CurrentTimeStamp(); } - public function count($col = NULL): Keyword { - if (is_null($col)) { - return new Keyword("COUNT(*) AS count"); - } else if($col instanceof Keyword) { - return new Keyword("COUNT(" . $col->getValue() . ") AS count"); - } else { - $countCol = strtolower(str_replace(".","_", $col)) . "_count"; - $col = $this->columnName($col); - return new Keyword("COUNT($col) AS $countCol"); - } - } - - public function distinct($col): Keyword { - $col = $this->columnName($col); - return new Keyword("DISTINCT($col)"); - } - // Statements /** * @return mixed */ protected abstract function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE); - public function buildCondition($condition, &$params) { + public function buildCondition(Condition|array $condition, &$params): string { - if ($condition instanceof CondOr) { - $conditions = array(); - foreach($condition->getConditions() as $cond) { - $conditions[] = $this->buildCondition($cond, $params); - } - return "(" . implode(" OR ", $conditions) . ")"; - } else if ($condition instanceof CondAnd) { - $conditions = array(); - foreach($condition->getConditions() as $cond) { - $conditions[] = $this->buildCondition($cond, $params); - } - return "(" . implode(" AND ", $conditions) . ")"; - } else if ($condition instanceof Compare) { - $column = $this->columnName($condition->getColumn()); - $value = $condition->getValue(); - $operator = $condition->getOperator(); - - if ($value === null) { - if ($operator === "=") { - return "$column IS NULL"; - } else if ($operator === "!=") { - return "$column IS NOT NULL"; - } - } - - return $column . $operator . $this->addValue($value, $params); - } else if ($condition instanceof CondBool) { - return $this->columnName($condition->getValue()); - } else if (is_array($condition)) { + if (is_array($condition)) { if (count($condition) === 1) { return $this->buildCondition($condition[0], $params); } else { @@ -336,77 +284,8 @@ abstract class SQL { } return implode(" AND ", $conditions); } - } else if($condition instanceof CondIn) { - - $needle = $condition->getNeedle(); - $haystack = $condition->getHaystack(); - if (is_array($haystack)) { - $values = array(); - foreach ($haystack as $value) { - $values[] = $this->addValue($value, $params); - } - - $values = implode(",", $values); - } else if($haystack instanceof Select) { - $values = $haystack->build($params); - } else { - $this->lastError = $this->logger->error("Unsupported in-expression value: " . get_class($condition)); - return false; - } - - if ($needle instanceof Column) { - $lhs = $this->createExpression($needle, $params); - } else { - $lhs = $this->addValue($needle, $params); - } - - return "$lhs IN ($values)"; - } else if($condition instanceof CondKeyword) { - $left = $condition->getLeftExp(); - $right = $condition->getRightExp(); - $keyword = $condition->getKeyword(); - $left = ($left instanceof Column) ? $this->columnName($left->getName()) : $this->addValue($left, $params); - $right = ($right instanceof Column) ? $this->columnName($right->getName()) : $this->addValue($right, $params); - return "$left $keyword $right "; - } else if($condition instanceof CondNot) { - $expression = $condition->getExpression(); - if ($expression instanceof Condition) { - $expression = $this->buildCondition($expression, $params); - } else { - $expression = $this->columnName($expression); - } - - return "NOT $expression"; - } else if ($condition instanceof CondNull) { - return $this->columnName($condition->getColumn()) . " IS NULL"; - } else if ($condition instanceof Exists) { - return "EXISTS(" .$condition->getSubQuery()->build($params) . ")"; } else { - $this->lastError = $this->logger->error("Unsupported condition type: " . gettype($condition)); - return null; - } - } - - protected function createExpression(Expression $exp, array &$params): ?string { - if ($exp instanceof Column) { - return $this->columnName($exp->getName()); - } else if ($exp instanceof Query) { - return "(" . $exp->build($params) . ")"; - } else if ($exp instanceof CaseWhen) { - $condition = $this->buildCondition($exp->getCondition(), $params); - - // psql requires constant values here - $trueCase = $this->addValue($exp->getTrueCase(), $params, true); - $falseCase = $this->addValue($exp->getFalseCase(), $params, true); - - return "CASE WHEN $condition THEN $trueCase ELSE $falseCase END"; - } else if ($exp instanceof Sum) { - $value = $this->addValue($exp->getValue(), $params); - $alias = $this->columnName($exp->getAlias()); - return "SUM($value) AS $alias"; - } else { - $this->lastError = $this->logger->error("Unsupported expression type: " . get_class($exp)); - return null; + return $this->addValue($condition, $params); } } @@ -441,8 +320,6 @@ abstract class SQL { return $sql; } - public abstract function getStatus(); - public function parseBool($val) : bool { return in_array($val, array(true, 1, '1', 't', 'true', 'TRUE'), true); } diff --git a/Core/Driver/SQL/Type/CurrentColumn.php b/Core/Driver/SQL/Type/CurrentColumn.php index 8081972..2de9a6e 100644 --- a/Core/Driver/SQL/Type/CurrentColumn.php +++ b/Core/Driver/SQL/Type/CurrentColumn.php @@ -8,7 +8,7 @@ use Core\Driver\SQL\Column\Column; class CurrentColumn extends Column { - public function __construct(string $string) { - parent::__construct($string); + public function __construct(string $name) { + parent::__construct($name); } } \ No newline at end of file diff --git a/Core/Driver/SQL/Type/CurrentTable.class.php b/Core/Driver/SQL/Type/CurrentTable.class.php index 458d239..5b38860 100644 --- a/Core/Driver/SQL/Type/CurrentTable.class.php +++ b/Core/Driver/SQL/Type/CurrentTable.class.php @@ -2,10 +2,23 @@ namespace Core\Driver\SQL\Type; -use Core\Driver\SQL\Column\StringColumn; +use Core\Driver\SQL\Expression\Expression; +use Core\Driver\SQL\MySQL; +use Core\Driver\SQL\PostgreSQL; +use Core\Driver\SQL\SQL; + +class CurrentTable extends Expression { -class CurrentTable extends StringColumn { public function __construct() { - parent::__construct("CURRENT_TABLE"); + } + + function getExpression(SQL $sql, array &$params): string { + if ($sql instanceof MySQL) { + // CURRENT_TABLE + } else if ($sql instanceof PostgreSQL) { + return "TG_TABLE_NAME"; + } else { + + } } } \ No newline at end of file diff --git a/Core/Localization/de_DE/general.php b/Core/Localization/de_DE/general.php index e3e48fb..74abdf5 100644 --- a/Core/Localization/de_DE/general.php +++ b/Core/Localization/de_DE/general.php @@ -12,4 +12,7 @@ return [ "loading" => "Laden", "logout" => "Ausloggen", "noscript" => "Sie müssen Javascript aktivieren um diese Anwendung zu benutzen", + + # data table + "showing_x_of_y_entries" => "Zeige %d von %d Einträgen", ]; \ No newline at end of file diff --git a/Core/Localization/en_US/general.php b/Core/Localization/en_US/general.php index 065df5b..ca39bca 100644 --- a/Core/Localization/en_US/general.php +++ b/Core/Localization/en_US/general.php @@ -12,4 +12,7 @@ return [ "loading" => "Loading", "logout" => "Logout", "noscript" => "You need Javascript enabled to run this app", + + # data table + "showing_x_of_y_entries" => "Showing %d of %d entries", ]; \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php index 13ba75a..b46d3b0 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php @@ -2,10 +2,14 @@ namespace Core\Objects\DatabaseEntity\Controller; +use ArrayAccess; use Core\Driver\SQL\Condition\Condition; +use Core\Driver\SQL\Expression\Count; use Core\Driver\SQL\SQL; +use Core\Objects\DatabaseEntity\Attribute\Transient; +use JsonSerializable; -abstract class DatabaseEntity { +abstract class DatabaseEntity implements ArrayAccess, JsonSerializable { protected static array $entityLogConfig = [ "insert" => false, @@ -16,11 +20,38 @@ abstract class DatabaseEntity { private static array $handlers = []; protected ?int $id; + #[Transient] protected array $customData = []; public function __construct(?int $id = null) { $this->id = $id; } + public function offsetExists(mixed $offset): bool { + return property_exists($this, $offset) || array_key_exists($offset, $this->customData); + } + + public function offsetGet(mixed $offset): mixed { + if (property_exists($this, $offset)) { + return $this->{$offset}; + } else { + return $this->customData[$offset]; + } + } + + public function offsetSet(mixed $offset, mixed $value): void { + if (property_exists($this, $offset)) { + $this->{$offset} = $value; + } else { + $this->customData[$offset] = $value; + } + } + + public function offsetUnset(mixed $offset): void { + if (array_key_exists($offset, $this->customData)) { + unset($this->customData[$offset]); + } + } + public abstract function jsonSerialize(): array; public function preInsert(array &$row) { } @@ -49,7 +80,7 @@ abstract class DatabaseEntity { public static function exists(SQL $sql, int $id): bool { $handler = self::getHandler($sql); - $res = $sql->select($sql->count()) + $res = $sql->select(new Count()) ->from($handler->getTableName()) ->whereEq($handler->getTableName() . ".id", $id) ->execute(); @@ -148,7 +179,7 @@ abstract class DatabaseEntity { public static function count(SQL $sql, ?Condition $condition = null): int|bool { $handler = self::getHandler($sql); - $query = $sql->select($sql->count()) + $query = $sql->select(new Count()) ->from($handler->getTableName()); if ($condition) { diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php index ae1199e..d3cdbd9 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php @@ -269,7 +269,7 @@ class DatabaseEntityHandler implements Persistable { return $rel_row; } - private function getValueFromRow(array $row, string $propertyName, mixed &$value): bool { + private function getValueFromRow(array $row, string $propertyName, mixed &$value, bool $initEntities = false): bool { $column = $this->columns[$propertyName] ?? null; if (!$column) { return false; @@ -290,8 +290,12 @@ class DatabaseEntityHandler implements Persistable { if (array_key_exists($relColumnPrefix . "id", $row)) { $relId = $row[$relColumnPrefix . "id"]; if ($relId !== null) { - $relationHandler = $this->relations[$propertyName]; - $value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix)); + if ($initEntities) { + $relationHandler = $this->relations[$propertyName]; + $value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix), [], true); + } else { + return false; + } } else if (!$column->notNull()) { $value = null; } else { @@ -305,7 +309,7 @@ class DatabaseEntityHandler implements Persistable { return true; } - public function entityFromRow(array $row): ?DatabaseEntity { + public function entityFromRow(array $row, array $additionalColumns = [], bool $initEntities = false): ?DatabaseEntity { try { $constructorClass = $this->entityClass; @@ -324,12 +328,18 @@ class DatabaseEntityHandler implements Persistable { } foreach ($this->properties as $property) { - if ($this->getValueFromRow($row, $property->getName(), $value)) { + if ($this->getValueFromRow($row, $property->getName(), $value, $initEntities)) { $property->setAccessible(true); $property->setValue($entity, $value); } } + foreach ($additionalColumns as $column) { + if (!in_array($column, $this->columns) && !isset($this->properties[$column])) { + $entity[$column] = $row[$column]; + } + } + // init n:m / 1:n properties with empty arrays foreach ($this->nmRelations as $nmRelation) { foreach ($nmRelation->getProperties($this) as $property) { @@ -453,9 +463,12 @@ class DatabaseEntityHandler implements Persistable { if ($recursive) { foreach ($entities as $entity) { foreach ($this->relations as $propertyName => $relHandler) { - $relEntity = $this->properties[$propertyName]->getValue($entity); - if ($relEntity) { - $relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true); + $property = $this->properties[$propertyName]; + if ($property->isInitialized($entity) || true) { + $relEntity = $this->properties[$propertyName]->getValue($entity); + if ($relEntity) { + $relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true); + } } } } @@ -483,10 +496,10 @@ class DatabaseEntityHandler implements Persistable { ->addJoin(new InnerJoin($nmTable, "$nmTable.$refIdColumn", "$refTableName.id")) ->where(new CondIn(new Column($thisIdColumn), $entityIds)); - $relEntityQuery->addColumn($thisIdColumn); + $relEntityQuery->addSelectValue(new Column($thisIdColumn)); foreach ($dataColumns as $tableDataColumns) { foreach ($tableDataColumns as $columnName) { - $relEntityQuery->addColumn($columnName); + $relEntityQuery->addSelectValue(new Column($columnName)); } } @@ -500,7 +513,7 @@ class DatabaseEntityHandler implements Persistable { foreach ($rows as $row) { $relId = $row["id"]; if (!isset($relEntities[$relId])) { - $relEntity = $otherHandler->entityFromRow($row); + $relEntity = $otherHandler->entityFromRow($row, [], $recursive); $relEntities[$relId] = $relEntity; } diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php index 2df0cc8..bf6e171 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php @@ -3,8 +3,11 @@ namespace Core\Objects\DatabaseEntity\Controller; use Core\Driver\Logger\Logger; +use Core\Driver\SQL\Column\Column; +use Core\Driver\SQL\Expression\Alias; use Core\Driver\SQL\Query\Select; use Core\Driver\SQL\SQL; +use Core\External\PHPMailer\Exception; /** * this class is similar to \Driver\SQL\Query\Select but with reduced functionality @@ -20,6 +23,7 @@ class DatabaseEntityQuery extends Select { private DatabaseEntityHandler $handler; private int $resultType; private bool $logVerbose; + private array $additionalColumns; private int $fetchSubEntities; @@ -29,6 +33,7 @@ class DatabaseEntityQuery extends Select { $this->logger = new Logger("DB-EntityQuery", $handler->getSQL()); $this->resultType = $resultType; $this->logVerbose = false; + $this->additionalColumns = []; $this->from($handler->getTableName()); $this->fetchSubEntities = self::FETCH_NONE; @@ -37,6 +42,26 @@ class DatabaseEntityQuery extends Select { } } + public function addCustomValue(mixed $selectValue): Select { + if (is_string($selectValue)) { + $this->additionalColumns[] = $selectValue; + } else if ($selectValue instanceof Alias) { + $this->additionalColumns[] = $selectValue->getAlias(); + } else if ($selectValue instanceof Column) { + $this->additionalColumns[] = $selectValue->getName(); + } else { + $this->logger->debug("Cannot get selected column name from custom value of type: " . get_class($selectValue)); + return $this; + } + + $this->addSelectValue($selectValue); + return $this; + } + + public function getHandler(): DatabaseEntityHandler { + return $this->handler; + } + public function debug(): DatabaseEntityQuery { $this->logVerbose = true; return $this; @@ -112,7 +137,7 @@ class DatabaseEntityQuery extends Select { if ($this->resultType === SQL::FETCH_ALL) { $entities = []; foreach ($res as $row) { - $entity = $this->handler->entityFromRow($row); + $entity = $this->handler->entityFromRow($row, $this->additionalColumns, $this->fetchSubEntities !== self::FETCH_NONE); if ($entity) { $entities[$entity->getId()] = $entity; } @@ -124,7 +149,7 @@ class DatabaseEntityQuery extends Select { return $entities; } else if ($this->resultType === SQL::FETCH_ONE) { - $entity = $this->handler->entityFromRow($res); + $entity = $this->handler->entityFromRow($res, $this->additionalColumns, $this->fetchSubEntities !== self::FETCH_NONE); if ($entity instanceof DatabaseEntity && $this->fetchSubEntities !== self::FETCH_NONE) { $this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities === self::FETCH_RECURSIVE); } diff --git a/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php b/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php index d85a188..cebf0a0 100644 --- a/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php +++ b/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php @@ -104,6 +104,8 @@ class NMRelation implements Persistable { } public static function buildTableName(string ...$tables): string { + // in case of class passed here + $tables = array_map(function ($t) { return isClass($t) ? getClassName($t) : $t; }, $tables); sort($tables); return "NM_" . implode("_", $tables); } diff --git a/Core/Objects/DatabaseEntity/Group.class.php b/Core/Objects/DatabaseEntity/Group.class.php index bbbaa9b..fcda692 100644 --- a/Core/Objects/DatabaseEntity/Group.class.php +++ b/Core/Objects/DatabaseEntity/Group.class.php @@ -2,8 +2,11 @@ namespace Core\Objects\DatabaseEntity; +use Core\Driver\SQL\SQL; use Core\Objects\DatabaseEntity\Attribute\MaxLength; use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler; +use Core\Objects\DatabaseEntity\Controller\NMRelation; class Group extends DatabaseEntity { @@ -33,4 +36,11 @@ class Group extends DatabaseEntity { "color" => $this->color ]; } + + public function getMembers(SQL $sql): array { + $nmTable = NMRelation::buildTableName(User::class, Group::class); + return User::findBy(User::createBuilder($sql, false) + ->innerJoin($nmTable, "user_id", "User.id") + ->whereEq("group_id", $this->id)); + } } \ No newline at end of file diff --git a/Core/core.php b/Core/core.php index 2b0c83f..62bf7b4 100644 --- a/Core/core.php +++ b/Core/core.php @@ -227,7 +227,7 @@ function getClassPath($class, string $suffix = ".class"): string { if ($pathCount >= 3) { if (strcasecmp($pathParts[$pathCount - 3], "API") === 0) { $group = $pathParts[$pathCount - 2]; - if (strcasecmp($group, "Parameter") !== 0) { + if (strcasecmp($group, "Parameter") !== 0 && strcasecmp($group, "Traits") !== 0) { $pathParts = array_slice($pathParts, 0, $pathCount - 2); $pathParts[] = "${group}API"; } diff --git a/react/admin-panel/src/AdminDashboard.jsx b/react/admin-panel/src/AdminDashboard.jsx index ad58b25..436d9b7 100644 --- a/react/admin-panel/src/AdminDashboard.jsx +++ b/react/admin-panel/src/AdminDashboard.jsx @@ -11,7 +11,11 @@ import {LocaleContext} from "shared/locale"; import './res/adminlte.min.css'; // views +import View404 from "./views/404"; const Overview = lazy(() => import('./views/overview')); +const UserListView = lazy(() => import('./views/user-list')); +const GroupListView = lazy(() => import('./views/group-list')); +const EditGroupView = lazy(() => import('./views/group-edit')); export default function AdminDashboard(props) { @@ -22,14 +26,21 @@ export default function AdminDashboard(props) { const {currentLocale, requestModules, translate: L} = useContext(LocaleContext); - const showDialog = useCallback((message, title, options=["Close"], onOption = null) => { - setDialog({ show: true, message: message, title: title, options: options, onOption: onOption }); - }, []); - const hideDialog = useCallback(() => { setDialog({show: false}); }, []); + const showDialog = useCallback((message, title, options=["Close"], onOption = null) => { + setDialog({ + show: true, message: + message, + title: title, + options: options, + onOption: onOption, + onClose: hideDialog + }); + }, []); + useEffect(() => { requestModules(api, ["general", "admin"], currentLocale).then(data => { if (!data.success) { @@ -52,7 +63,12 @@ export default function AdminDashboard(props) {
{L("general.loading")}... }> + }/> }/> + }/> + }/> + }/> + } /> {/* diff --git a/react/admin-panel/src/App.jsx b/react/admin-panel/src/App.jsx index 33027ed..f555378 100644 --- a/react/admin-panel/src/App.jsx +++ b/react/admin-panel/src/App.jsx @@ -85,8 +85,6 @@ export default function App() { */ - console.log(loaded, user, api.loggedIn); - if (!loaded) { if (error) { return diff --git a/react/admin-panel/src/elements/sidebar.js b/react/admin-panel/src/elements/sidebar.js index 468ff95..ee37207 100644 --- a/react/admin-panel/src/elements/sidebar.js +++ b/react/admin-panel/src/elements/sidebar.js @@ -92,7 +92,9 @@ export default function Sidebar(props) {