From a39a3182deec6f1d97cad143f2bf62261886a7dc Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 10 Oct 2023 22:01:17 +1300 Subject: [PATCH 1/6] Allow adding hooks to transform queries before execution --- src/Database/Adapter.php | 38 ++++ src/Database/Adapter/MariaDB.php | 296 +++++++++++++++++++++--------- src/Database/Adapter/Postgres.php | 271 ++++++++++++++++++--------- src/Database/Adapter/SQLite.php | 152 ++++++++++----- src/Database/Database.php | 58 ++++-- 5 files changed, 591 insertions(+), 224 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index decfd7db4..ee132c71b 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -22,6 +22,13 @@ abstract class Adapter */ protected array $debug = []; + /** + * @var array> + */ + protected array $transformations = [ + '*' => [], + ]; + protected static ?int $timeout = null; /** @@ -135,6 +142,37 @@ public function getDefaultDatabase(): string return $this->defaultDatabase; } + + /** + * Apply a transformation to a query before an event occurs + * + * @param string $event + * @param string $name + * @param ?callable $callback + * @return self + */ + public function before(string $event, string $name = '', ?callable $callback = null): self + { + if (!isset($this->transformations[$event])) { + $this->transformations[$event] = []; + } + $this->transformations[$event][$name] = $callback; + + return $this; + } + + protected function trigger(string $event, mixed $query): mixed + { + foreach ($this->transformations[Database::EVENT_ALL] as $callback) { + $query = $callback($query); + } + foreach (($this->transformations[$event] ?? []) as $callback) { + $query = $callback($query); + } + + return $query; + } + /** * Ping Database * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index d4647f754..e4ceb8a33 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -27,8 +27,12 @@ public function create(string $name): bool { $name = $this->filter($name); + $sql = "CREATE DATABASE IF NOT EXISTS `{$name}` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;"; + + $sql = $this->trigger(Database::EVENT_DATABASE_CREATE, $sql); + return $this->getPDO() - ->prepare("CREATE DATABASE IF NOT EXISTS `{$name}` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;") + ->prepare($sql) ->execute(); } @@ -44,8 +48,12 @@ public function delete(string $name): bool { $name = $this->filter($name); + $sql = "DROP DATABASE `{$name}`;"; + + $sql = $this->trigger(Database::EVENT_DATABASE_DELETE, $sql); + return $this->getPDO() - ->prepare("DROP DATABASE `{$name}`;") + ->prepare($sql) ->execute(); } @@ -103,33 +111,45 @@ public function createCollection(string $name, array $attributes = [], array $in $indexStrings[$key] = "{$indexType} `{$indexId}` (" . \implode(", ", $indexAttributes) . " ),"; } + $sql = " + CREATE TABLE IF NOT EXISTS `{$database}`.`{$namespace}_{$id}` ( + `_id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `_uid` VARCHAR(255) NOT NULL, + `_createdAt` datetime(3) DEFAULT NULL, + `_updatedAt` datetime(3) DEFAULT NULL, + `_permissions` MEDIUMTEXT DEFAULT NULL, + " . \implode(' ', $attributeStrings) . " + PRIMARY KEY (`_id`), + " . \implode(' ', $indexStrings) . " + UNIQUE KEY `_uid` (`_uid`), + KEY `_created_at` (`_createdAt`), + KEY `_updated_at` (`_updatedAt`) + ) + "; + + $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + try { $this->getPDO() - ->prepare("CREATE TABLE IF NOT EXISTS `{$database}`.`{$namespace}_{$id}` ( - `_id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `_uid` VARCHAR(255) NOT NULL, - `_createdAt` datetime(3) DEFAULT NULL, - `_updatedAt` datetime(3) DEFAULT NULL, - `_permissions` MEDIUMTEXT DEFAULT NULL, - " . \implode(' ', $attributeStrings) . " - PRIMARY KEY (`_id`), - " . \implode(' ', $indexStrings) . " - UNIQUE KEY `_uid` (`_uid`), - KEY `_created_at` (`_createdAt`), - KEY `_updated_at` (`_updatedAt`) - )") + ->prepare($sql) ->execute(); + $sql = " + CREATE TABLE IF NOT EXISTS `{$database}`.`{$namespace}_{$id}_perms` ( + `_id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `_type` VARCHAR(12) NOT NULL, + `_permission` VARCHAR(255) NOT NULL, + `_document` VARCHAR(255) NOT NULL, + PRIMARY KEY (`_id`), + UNIQUE INDEX `_index1` (`_document`,`_type`,`_permission`), + INDEX `_permission` (`_permission`,`_type`,`_document`) + ) + "; + + $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + $this->getPDO() - ->prepare("CREATE TABLE IF NOT EXISTS `{$database}`.`{$namespace}_{$id}_perms` ( - `_id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `_type` VARCHAR(12) NOT NULL, - `_permission` VARCHAR(255) NOT NULL, - `_document` VARCHAR(255) NOT NULL, - PRIMARY KEY (`_id`), - UNIQUE INDEX `_index1` (`_document`,`_type`,`_permission`), - INDEX `_permission` (`_permission`,`_type`,`_document`) - )") + ->prepare($sql) ->execute(); } catch (\Exception $th) { $this->getPDO() @@ -138,7 +158,6 @@ public function createCollection(string $name, array $attributes = [], array $in throw $th; } - // Update $this->getCountOfIndexes when adding another default index return true; } @@ -193,8 +212,12 @@ public function deleteCollection(string $id): bool { $id = $this->filter($id); + $sql = "DROP TABLE {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};"; + + $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); + return $this->getPDO() - ->prepare("DROP TABLE {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + ->prepare($sql) ->execute(); } @@ -221,8 +244,11 @@ public function createAttribute(string $collection, string $id, string $type, in $type = 'LONGTEXT'; } + $sql = "ALTER TABLE {$this->getSQLTable($name)} ADD COLUMN `{$id}` {$type};"; + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($name)} ADD COLUMN `{$id}` {$type};") + ->prepare($sql) ->execute(); } @@ -249,8 +275,12 @@ public function updateAttribute(string $collection, string $id, string $type, in $type = 'LONGTEXT'; } + $sql = "ALTER TABLE {$this->getSQLTable($name)} MODIFY `{$id}` {$type};"; + + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($name)} MODIFY `{$id}` {$type};") + ->prepare($sql) ->execute(); } @@ -269,8 +299,12 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa $name = $this->filter($collection); $id = $this->filter($id); + $sql = "ALTER TABLE {$this->getSQLTable($name)} DROP COLUMN `{$id}`;"; + + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($name)} DROP COLUMN `{$id}`;") + ->prepare($sql) ->execute(); } @@ -290,8 +324,12 @@ public function renameAttribute(string $collection, string $old, string $new): b $old = $this->filter($old); $new = $this->filter($new); + $sql = "ALTER TABLE {$this->getSQLTable($collection)} RENAME COLUMN `{$old}` TO `{$new}`;"; + + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($collection)} RENAME COLUMN `{$old}` TO `{$new}`;") + ->prepare($sql) ->execute(); } @@ -341,6 +379,8 @@ public function createRelationship( throw new DatabaseException('Invalid relationship type'); } + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql); + return $this->getPDO() ->prepare($sql) ->execute(); @@ -424,6 +464,8 @@ public function updateRelationship( return true; } + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); + return $this->getPDO() ->prepare($sql) ->execute(); @@ -490,6 +532,8 @@ public function deleteRelationship( return true; } + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql); + return $this->getPDO() ->prepare($sql) ->execute(); @@ -510,8 +554,12 @@ public function renameIndex(string $collection, string $old, string $new): bool $old = $this->filter($old); $new = $this->filter($new); + $sql = "ALTER TABLE {$this->getSQLTable($collection)} RENAME INDEX `{$old}` TO `{$new}`;"; + + $sql = $this->trigger(Database::EVENT_INDEX_RENAME, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($collection)} RENAME INDEX `{$old}` TO `{$new}`;") + ->prepare($sql) ->execute(); } @@ -553,8 +601,12 @@ public function createIndex(string $collection, string $id, string $type, array $attributes[$key] = "`{$attribute}`{$length} {$order}"; } + $sql = $this->getSQLIndex($name, $id, $type, $attributes); + + $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); + return $this->getPDO() - ->prepare($this->getSQLIndex($name, $id, $type, $attributes)) + ->prepare($sql) ->execute(); } @@ -572,8 +624,12 @@ public function deleteIndex(string $collection, string $id): bool $name = $this->filter($collection); $id = $this->filter($id); + $sql = "ALTER TABLE {$this->getSQLTable($name)} DROP INDEX `{$id}`;"; + + $sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($name)} DROP INDEX `{$id}`;") + ->prepare($sql) ->execute(); } @@ -619,9 +675,14 @@ public function createDocument(string $collection, Document $document): Document $columnNames .= ':' . $bindKey . ', '; } - $stmt = $this->getPDO() - ->prepare("INSERT INTO {$this->getSQLTable($name)} - ({$columns}_uid) VALUES ({$columnNames}:_uid)"); + $sql = " + INSERT INTO {$this->getSQLTable($name)}({$columns} _uid) + VALUES ({$columnNames} :_uid) + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_CREATE, $sql); + + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR); @@ -652,8 +713,14 @@ public function createDocument(string $collection, Document $document): Document } if (!empty($permissions)) { - $queryPermissions = "INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document) VALUES " . implode(', ', $permissions); - $stmtPermissions = $this->getPDO()->prepare($queryPermissions); + $strPermissions = \implode(', ', $permissions); + + $sqlPermissions = " + INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document) + VALUES {$strPermissions} + "; + $sqlPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sqlPermissions); + $stmtPermissions = $this->getPDO()->prepare($sqlPermissions); } try { @@ -703,17 +770,21 @@ public function updateDocument(string $collection, Document $document): Document $name = $this->filter($collection); $columns = ''; + $sql = " + SELECT _type, _permission + FROM {$this->getSQLTable($name . '_perms')} p + WHERE p._document = :_uid + "; + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); + /** * Get current permissions from the database */ - $permissionsStmt = $this->getPDO()->prepare(" - SELECT _type, _permission - FROM {$this->getSQLTable($name . '_perms')} p - WHERE p._document = :_uid - "); - $permissionsStmt->bindValue(':_uid', $document->getId()); - $permissionsStmt->execute(); - $permissions = $permissionsStmt->fetchAll(); + $sqlPermissions = $this->getPDO()->prepare($sql); + $sqlPermissions->bindValue(':_uid', $document->getId()); + $sqlPermissions->execute(); + $permissions = $sqlPermissions->fetchAll(); $initial = []; foreach (Database::PERMISSIONS as $type) { @@ -768,14 +839,17 @@ public function updateDocument(string $collection, Document $document): Document } if (!empty($removeQuery)) { $removeQuery .= ')'; - $stmtRemovePermissions = $this->getPDO() - ->prepare(" - DELETE + $removeQuery = " + DELETE FROM {$this->getSQLTable($name . '_perms')} WHERE _document = :_uid {$removeQuery} - "); + "; + + $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); + + $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); $stmtRemovePermissions->bindValue(':_uid', $document->getId()); foreach ($removals as $type => $permissions) { @@ -796,12 +870,14 @@ public function updateDocument(string $collection, Document $document): Document } } - $stmtAddPermissions = $this->getPDO() - ->prepare( - " - INSERT INTO {$this->getSQLTable($name . '_perms')} - (_document, _type, _permission) VALUES " . \implode(', ', $values) - ); + $sql = " + INSERT INTO {$this->getSQLTable($name . '_perms')} + (_document, _type, _permission) VALUES " . \implode(', ', $values) + ; + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql); + + $stmtAddPermissions = $this->getPDO()->prepare($sql); $stmtAddPermissions->bindValue(":_uid", $document->getId()); foreach ($additions as $type => $permissions) { @@ -823,9 +899,15 @@ public function updateDocument(string $collection, Document $document): Document $bindIndex++; } - $stmt = $this->getPDO() - ->prepare("UPDATE {$this->getSQLTable($name)} - SET {$columns} _uid = :_uid WHERE _uid = :_uid"); + $sql = " + UPDATE {$this->getSQLTable($name)} + SET {$columns} _uid = :_uid + WHERE _uid = :_uid + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); + + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $document->getId()); @@ -844,6 +926,7 @@ public function updateDocument(string $collection, Document $document): Document try { $stmt->execute(); + if (isset($stmtRemovePermissions)) { $stmtRemovePermissions->execute(); } @@ -886,10 +969,20 @@ public function increaseDocumentAttribute(string $collection, string $id, string $name = $this->filter($collection); $attribute = $this->filter($attribute); - $sqlMax = $max ? " AND `{$attribute}` <= {$max}" : ""; - $sqlMin = $min ? " AND `{$attribute}` >= {$min}" : ""; + $sqlMax = $max ? " AND `{$attribute}` <= {$max}" : ''; + $sqlMin = $min ? " AND `{$attribute}` >= {$min}" : ''; + + $sql = " + UPDATE {$this->getSQLTable($name)} + SET `{$attribute}` = `{$attribute}` + :val + WHERE + _uid = :_uid + {$sqlMax} + {$sqlMin} + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); - $sql = "UPDATE {$this->getSQLTable($name)} SET `{$attribute}` = `{$attribute}` + :val WHERE _uid = :_uid" . $sqlMax . $sqlMin; $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $id); $stmt->bindValue(':val', $value); @@ -913,15 +1006,34 @@ public function deleteDocument(string $collection, string $id): bool $this->getPDO()->beginTransaction(); - $stmt = $this->getPDO()->prepare("DELETE FROM {$this->getSQLTable($name)} WHERE _uid = :_uid"); + $sql = " + DELETE FROM {$this->getSQLTable($name)} + WHERE _uid = :_uid + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql); + + $stmt = $this->getPDO()->prepare($sql); + $stmt->bindValue(':_uid', $id); - $stmtPermissions = $this->getPDO()->prepare("DELETE FROM {$this->getSQLTable($name . '_perms')} WHERE _document = :_uid"); + $sql = " + DELETE FROM {$this->getSQLTable($name . '_perms')} + WHERE _document = :_uid + "; + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); + + $stmtPermissions = $this->getPDO()->prepare($sql); $stmtPermissions->bindValue(':_uid', $id); try { - $stmt->execute() || throw new DatabaseException('Failed to delete document'); - $stmtPermissions->execute() || throw new DatabaseException('Failed to clean permissions'); + if (!$stmt->execute()) { + throw new DatabaseException('Failed to delete document'); + } + if (!$stmtPermissions->execute()) { + throw new DatabaseException('Failed to clean permissions'); + } } catch (\Throwable $th) { $this->getPDO()->rollBack(); throw new DatabaseException($th->getMessage()); @@ -1058,6 +1170,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, {$sqlLimit}; "; + $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); + if ($timeout || static::$timeout) { $sql = $this->setTimeoutForQuery($sql, $timeout ? $timeout : static::$timeout); } @@ -1078,7 +1192,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, default => $attribute }; - if (is_null($cursor[$attribute] ?? null)) { + if (\is_null($cursor[$attribute] ?? null)) { throw new DatabaseException("Order attribute '{$attribute}' is empty"); } $stmt->bindValue(':cursor', $cursor[$attribute], $this->getPDOType($cursor[$attribute])); @@ -1125,7 +1239,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } if ($cursorDirection === Database::CURSOR_BEFORE) { - $results = array_reverse($results); + $results = \array_reverse($results); } return $results; @@ -1156,16 +1270,21 @@ public function count(string $collection, array $queries = [], ?int $max = null, $where[] = $this->getSQLPermissionsCondition($name, $roles); } - $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; - $sql = "SELECT COUNT(1) as sum - FROM - ( - SELECT 1 - FROM {$this->getSQLTable($name)} table_main - " . $sqlWhere . " - {$limit} - ) table_count + $sqlWhere = !empty($where) + ? 'WHERE ' . \implode(' AND ', $where) + : ''; + + $sql = " + SELECT COUNT(1) as sum FROM ( + SELECT 1 + FROM {$this->getSQLTable($name)} table_main + " . $sqlWhere . " + {$limit} + ) table_count "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_COUNT, $sql); + if ($timeout || self::$timeout) { $sql = $this->setTimeoutForQuery($sql, $timeout ? $timeout : self::$timeout); } @@ -1212,16 +1331,21 @@ public function sum(string $collection, string $attribute, array $queries = [], $where[] = $this->getSQLPermissionsCondition($name, $roles); } - $sqlWhere = !empty($where) ? 'where ' . implode(' AND ', $where) : ''; - $sql = "SELECT SUM({$attribute}) as sum - FROM - ( - SELECT {$attribute} - FROM {$this->getSQLTable($name)} table_main - " . $sqlWhere . " - {$limit} - ) table_count + $sqlWhere = !empty($where) + ? 'WHERE ' . \implode(' AND ', $where) + : ''; + + $sql = " + SELECT SUM({$attribute}) as sum FROM ( + SELECT {$attribute} + FROM {$this->getSQLTable($name)} table_main + {$sqlWhere} + {$limit} + ) table_count "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_SUM, $sql); + if ($timeout || self::$timeout) { $sql = $this->setTimeoutForQuery($sql, $timeout ? $timeout : self::$timeout); } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 8df026bda..d1bf848e1 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -36,8 +36,11 @@ public function create(string $name): bool { $name = $this->filter($name); + $sql = "CREATE SCHEMA IF NOT EXISTS \"{$name}\""; + $sql = $this->trigger(Database::EVENT_DATABASE_CREATE, $sql); + return $this->getPDO() - ->prepare("CREATE SCHEMA IF NOT EXISTS \"{$name}\"") + ->prepare($sql) ->execute(); } @@ -52,8 +55,12 @@ public function create(string $name): bool public function delete(string $name): bool { $name = $this->filter($name); + + $sql = "DROP SCHEMA IF EXISTS \"{$name}\" CASCADE"; + $sql = $this->trigger(Database::EVENT_DATABASE_DELETE, $sql); + return $this->getPDO() - ->prepare("DROP SCHEMA \"{$name}\" CASCADE;") + ->prepare($sql) ->execute(); } @@ -87,7 +94,7 @@ public function createCollection(string $name, array $attributes = [], array $in /** * @var array $attributes */ - $stmt = $this->getPDO()->prepare(" + $sql = " CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id)} ( \"_id\" SERIAL NOT NULL, \"_uid\" VARCHAR(255) NOT NULL, @@ -97,29 +104,37 @@ public function createCollection(string $name, array $attributes = [], array $in " . \implode(' ', $attributes) . " PRIMARY KEY (\"_id\") ); - "); - - $stmtIndex = $this->getPDO()->prepare(" - CREATE UNIQUE INDEX \"{$namespace}_{$id}_uid\" on {$this->getSQLTable($id)} (LOWER(\"_uid\")); + + CREATE UNIQUE INDEX \"{$namespace}_{$id}_uid\" on {$this->getSQLTable($id)} (LOWER(\"_uid\")); CREATE INDEX \"{$namespace}_{$id}_created\" ON {$this->getSQLTable($id)} (\"_createdAt\"); CREATE INDEX \"{$namespace}_{$id}_updated\" ON {$this->getSQLTable($id)} (\"_updatedAt\"); - "); + "; + + $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + + $stmt = $this->getPDO()->prepare($sql); try { $stmt->execute(); - $stmtIndex->execute(); + + $sql = " + CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id . '_perms')} ( + \"_id\" SERIAL NOT NULL, + \"_type\" VARCHAR(12) NOT NULL, + \"_permission\" VARCHAR(255) NOT NULL, + \"_document\" VARCHAR(255) NOT NULL, + PRIMARY KEY (\"_id\") + ); + CREATE UNIQUE INDEX \"index_{$namespace}_{$id}_ukey\" + ON {$this->getSQLTable($id. '_perms')} USING btree (\"_document\",\"_type\",\"_permission\"); + CREATE INDEX \"index_{$namespace}_{$id}_permission\" + ON {$this->getSQLTable($id. '_perms')} USING btree (\"_permission\",\"_type\",\"_document\"); + "; + + $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); $this->getPDO() - ->prepare("CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id . '_perms')} ( - \"_id\" SERIAL NOT NULL, - \"_type\" VARCHAR(12) NOT NULL, - \"_permission\" VARCHAR(255) NOT NULL, - \"_document\" VARCHAR(255) NOT NULL, - PRIMARY KEY (\"_id\") - ); - CREATE UNIQUE INDEX \"index_{$namespace}_{$id}_ukey\" ON {$this->getSQLTable($id. '_perms')} USING btree (\"_document\",\"_type\",\"_permission\"); - CREATE INDEX \"index_{$namespace}_{$id}_permission\" ON {$this->getSQLTable($id. '_perms')} USING btree (\"_permission\",\"_type\",\"_document\"); - ") + ->prepare($sql) ->execute(); foreach ($indexes as $index) { @@ -146,8 +161,6 @@ public function createCollection(string $name, array $attributes = [], array $in throw new DatabaseException('Failed to commit transaction'); } - // Update $this->getIndexCount when adding another default index - return true; } @@ -196,8 +209,11 @@ public function deleteCollection(string $id): bool { $id = $this->filter($id); + $sql = "DROP TABLE {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')}"; + $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); + return $this->getPDO() - ->prepare("DROP TABLE {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + ->prepare($sql) ->execute(); } @@ -222,9 +238,15 @@ public function createAttribute(string $collection, string $id, string $type, in $type = 'TEXT'; } + $sql = " + ALTER TABLE {$this->getSQLTable($name)} + ADD COLUMN \"{$id}\" {$type} + "; + + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($name)} - ADD COLUMN \"{$id}\" {$type};") + ->prepare($sql) ->execute(); } @@ -242,9 +264,15 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa $name = $this->filter($collection); $id = $this->filter($id); + $sql = " + ALTER TABLE {$this->getSQLTable($name)} + DROP COLUMN \"{$id}\"; + "; + + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($name)} - DROP COLUMN \"{$id}\";") + ->prepare($sql) ->execute(); } @@ -264,11 +292,15 @@ public function renameAttribute(string $collection, string $old, string $new): b $old = $this->filter($old); $new = $this->filter($new); + $sql = " + ALTER TABLE {$this->getSQLTable($collection)} + RENAME COLUMN \"{$old}\" TO \"{$new}\" + "; + + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($collection)} RENAME COLUMN - \"{$old}\" - TO - \"{$new}\";") + ->prepare($sql) ->execute(); } @@ -299,9 +331,15 @@ public function updateAttribute(string $collection, string $id, string $type, in $type = "TIMESTAMP(3) without time zone USING TO_TIMESTAMP(\"$id\", 'YYYY-MM-DD HH24:MI:SS.MS')"; } + $sql = " + ALTER TABLE {$this->getSQLTable($name)} + ALTER COLUMN \"{$id}\" TYPE {$type} + "; + + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($name)} - ALTER COLUMN \"{$id}\" TYPE {$type};") + ->prepare($sql) ->execute(); } @@ -351,6 +389,8 @@ public function createRelationship( throw new DatabaseException('Invalid relationship type'); } + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql); + return $this->getPDO() ->prepare($sql) ->execute(); @@ -434,6 +474,8 @@ public function updateRelationship( return true; } + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); + return $this->getPDO() ->prepare($sql) ->execute(); @@ -493,6 +535,8 @@ public function deleteRelationship( throw new DatabaseException('Invalid relationship type'); } + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql); + return $this->getPDO() ->prepare($sql) ->execute(); @@ -539,8 +583,11 @@ public function createIndex(string $collection, string $id, string $type, array } } + $sql = $this->getSQLIndex($name, $id, $type, $attributes); + $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); + return $this->getPDO() - ->prepare($this->getSQLIndex($name, $id, $type, $attributes)) + ->prepare($sql) ->execute(); } @@ -559,8 +606,11 @@ public function deleteIndex(string $collection, string $id): bool $id = $this->filter($id); $schemaName = $this->getDefaultDatabase(); + $sql = "DROP INDEX IF EXISTS \"{$schemaName}\".{$id}"; + $sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql); + return $this->getPDO() - ->prepare("DROP INDEX IF EXISTS \"{$schemaName}\".{$id};") + ->prepare($sql) ->execute(); } @@ -583,8 +633,11 @@ public function renameIndex(string $collection, string $old, string $new): bool $oldIndexName = $collection . "_" . $old; $newIndexName = $namespace . $collection . "_" . $new; + $sql = "ALTER INDEX {$this->getSQLTable($oldIndexName)} RENAME TO \"{$newIndexName}\""; + $sql = $this->trigger(Database::EVENT_INDEX_RENAME, $sql); + return $this->getPDO() - ->prepare("ALTER INDEX {$this->getSQLTable($oldIndexName)} RENAME TO \"{$newIndexName}\";") + ->prepare($sql) ->execute(); } @@ -629,9 +682,14 @@ public function createDocument(string $collection, Document $document): Document $bindIndex++; } - $stmt = $this->getPDO() - ->prepare("INSERT INTO {$this->getSQLTable($name)} - ({$columns}\"_uid\") VALUES ({$columnNames}:_uid) RETURNING _id;"); + $sql = " + INSERT INTO {$this->getSQLTable($name)} ({$columns}\"_uid\") + VALUES ({$columnNames}:_uid) RETURNING _id + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_CREATE, $sql); + + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR); @@ -663,8 +721,12 @@ public function createDocument(string $collection, Document $document): Document if (!empty($permissions)) { - $queryPermissions = "INSERT INTO {$this->getSQLTable($name . '_perms')} - (_type, _permission, _document) VALUES " . implode(', ', $permissions); + $queryPermissions = " + INSERT INTO {$this->getSQLTable($name . '_perms')} + (_type, _permission, _document) VALUES " . implode(', ', $permissions); + + $queryPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $queryPermissions); + $stmtPermissions = $this->getPDO()->prepare($queryPermissions); } @@ -712,14 +774,18 @@ public function updateDocument(string $collection, Document $document): Document $name = $this->filter($collection); $columns = ''; + $sql = " + SELECT _type, _permission + FROM {$this->getSQLTable($name . '_perms')} p + WHERE p._document = :_uid + "; + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); + /** * Get current permissions from the database */ - $permissionsStmt = $this->getPDO()->prepare(" - SELECT _type, _permission - FROM {$this->getSQLTable($name . '_perms')} p - WHERE p._document = :_uid - "); + $permissionsStmt = $this->getPDO()->prepare($sql); $permissionsStmt->bindValue(':_uid', $document->getId()); $permissionsStmt->execute(); $permissions = $permissionsStmt->fetchAll(); @@ -777,14 +843,15 @@ public function updateDocument(string $collection, Document $document): Document } if (!empty($removeQuery)) { $removeQuery .= ')'; - $stmtRemovePermissions = $this->getPDO() - ->prepare(" - DELETE + $removeQuery = " + DELETE FROM {$this->getSQLTable($name . '_perms')} WHERE _document = :_uid {$removeQuery} - "); + "; + $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); + $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); $stmtRemovePermissions->bindValue(':_uid', $document->getId()); foreach ($removals as $type => $permissions) { @@ -805,11 +872,13 @@ public function updateDocument(string $collection, Document $document): Document } } - $stmtAddPermissions = $this->getPDO() - ->prepare( - "INSERT INTO {$this->getSQLTable($name . '_perms')} - (_document, _type, _permission) VALUES" . \implode(', ', $values) - ); + $sql = " + INSERT INTO {$this->getSQLTable($name . '_perms')} + (_document, _type, _permission) VALUES" . \implode(', ', $values); + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql); + + $stmtAddPermissions = $this->getPDO()->prepare($sql); $stmtAddPermissions->bindValue(":_uid", $document->getId()); foreach ($additions as $type => $permissions) { @@ -831,9 +900,14 @@ public function updateDocument(string $collection, Document $document): Document $bindIndex++; } - $stmt = $this->getPDO() - ->prepare("UPDATE {$this->getSQLTable($name)} - SET {$columns} _uid = :_uid WHERE _uid = :_uid"); + $sql = " + UPDATE {$this->getSQLTable($name)} + SET {$columns} _uid = :_uid WHERE _uid = :_uid + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); + + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $document->getId()); @@ -897,7 +971,17 @@ public function increaseDocumentAttribute(string $collection, string $id, string $sqlMax = $max ? " AND \"{$attribute}\" <= {$max}" : ""; $sqlMin = $min ? " AND \"{$attribute}\" >= {$min}" : ""; - $sql = "UPDATE {$this->getSQLTable($name)} SET \"{$attribute}\" = \"{$attribute}\" + :val WHERE _uid = :_uid" . $sqlMax . $sqlMin; + $sql = " + UPDATE {$this->getSQLTable($name)} + SET \"{$attribute}\" = \"{$attribute}\" + :val + WHERE + _uid = :_uid + {$sqlMax} + {$sqlMin} + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $id); $stmt->bindValue(':val', $value); @@ -920,17 +1004,34 @@ public function deleteDocument(string $collection, string $id): bool $this->getPDO()->beginTransaction(); - $stmt = $this->getPDO() - ->prepare("DELETE FROM {$this->getSQLTable($name)} WHERE _uid = :_uid"); + $sql = " + DELETE FROM {$this->getSQLTable($name)} + WHERE _uid = :_uid + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql); + + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $id, PDO::PARAM_STR); - $stmtPermissions = $this->getPDO()->prepare("DELETE FROM {$this->getSQLTable($name . '_perms')} WHERE _document = :_uid"); + $sql = " + DELETE FROM {$this->getSQLTable($name . '_perms')} + WHERE _document = :_uid + "; + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); + + $stmtPermissions = $this->getPDO()->prepare($sql); $stmtPermissions->bindValue(':_uid', $id); try { - $stmt->execute() || throw new DatabaseException('Failed to delete document'); - $stmtPermissions->execute() || throw new DatabaseException('Failed to clean permissions'); + if (!$stmt->execute()) { + throw new DatabaseException('Failed to delete document'); + } + if (!$stmtPermissions->execute()) { + throw new DatabaseException('Failed to clean permissions'); + } } catch (\Throwable $th) { $this->getPDO()->rollBack(); throw new DatabaseException($th->getMessage()); @@ -1064,11 +1165,14 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, {$sqlLimit}; "; + $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); + if ($timeout || self::$timeout) { - $sql = $this->setTimeoutForQuery($sql, $timeout ? $timeout : self::$timeout); + $sql = $this->setTimeoutForQuery($sql, $timeout ?: self::$timeout); } $stmt = $this->getPDO()->prepare($sql); + foreach ($queries as $query) { $this->bindConditionValue($stmt, $query); } @@ -1084,7 +1188,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, default => $attribute }; - if (is_null($cursor[$attribute] ?? null)) { + if (\is_null($cursor[$attribute] ?? null)) { throw new DatabaseException("Order attribute '{$attribute}' is empty."); } $stmt->bindValue(':cursor', $cursor[$attribute], $this->getPDOType($cursor[$attribute])); @@ -1164,21 +1268,23 @@ public function count(string $collection, array $queries = [], ?int $max = null, } $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; - $sql = "SELECT COUNT(1) as sum - FROM - ( - SELECT 1 - FROM {$this->getSQLTable($name)} table_main - " . $sqlWhere . " - {$limit} - ) table_count + $sql = " + SELECT COUNT(1) as sum FROM ( + SELECT 1 + FROM {$this->getSQLTable($name)} table_main + " . $sqlWhere . " + {$limit} + ) table_count "; + $sql = $this->trigger(Database::EVENT_DOCUMENT_COUNT, $sql); + if ($timeout || self::$timeout) { - $sql = $this->setTimeoutForQuery($sql, $timeout ? $timeout : self::$timeout); + $sql = $this->setTimeoutForQuery($sql, $timeout ?: self::$timeout); } $stmt = $this->getPDO()->prepare($sql); + foreach ($queries as $query) { $this->bindConditionValue($stmt, $query); } @@ -1223,16 +1329,17 @@ public function sum(string $collection, string $attribute, array $queries = [], $where[] = $this->getSQLPermissionsCondition($name, $roles); } - $sql = "SELECT SUM({$attribute}) as sum - FROM - ( - SELECT {$attribute} - FROM {$this->getSQLTable($name)} table_main - WHERE {$permissions} AND " . implode(' AND ', $where) . " - {$limit} - ) table_count + $sql = " + SELECT SUM({$attribute}) as sum FROM ( + SELECT {$attribute} + FROM {$this->getSQLTable($name)} table_main + WHERE {$permissions} AND " . implode(' AND ', $where) . " + {$limit} + ) table_count "; + $sql = $this->trigger(Database::EVENT_DOCUMENT_SUM, $sql); + if ($timeout || self::$timeout) { $sql = $this->setTimeoutForQuery($sql, $timeout ? $timeout : self::$timeout); } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 932de4f3f..46c23c95c 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -47,7 +47,14 @@ public function exists(string $database, ?string $collection): bool $collection = $this->filter($collection); - $stmt = $this->getPDO()->prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = :table"); + $sql = " + SELECT name FROM sqlite_master + WHERE type='table' AND name = :table + "; + + $sql = $this->trigger(Database::EVENT_DATABASE_CREATE, $sql); + + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':table', "{$this->getNamespace()}_{$collection}", PDO::PARAM_STR); @@ -115,15 +122,21 @@ public function createCollection(string $name, array $attributes = [], array $in $attributeStrings[$key] = "`{$attrId}` {$attrType}, "; } + $sql = " + CREATE TABLE IF NOT EXISTS `{$namespace}_{$id}` ( + `_id` INTEGER PRIMARY KEY AUTOINCREMENT, + `_uid` CHAR(255) NOT NULL, + `_createdAt` datetime(3) DEFAULT NULL, + `_updatedAt` datetime(3) DEFAULT NULL, + `_permissions` MEDIUMTEXT DEFAULT NULL".(!empty($attributes) ? ',' : '')." + " . \substr(\implode(' ', $attributeStrings), 0, -2) . " + ) + "; + + $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + $this->getPDO() - ->prepare("CREATE TABLE IF NOT EXISTS `{$namespace}_{$id}` ( - `_id` INTEGER PRIMARY KEY AUTOINCREMENT, - `_uid` CHAR(255) NOT NULL, - `_createdAt` datetime(3) DEFAULT NULL, - `_updatedAt` datetime(3) DEFAULT NULL, - `_permissions` MEDIUMTEXT DEFAULT NULL".((!empty($attributes)) ? ',' : '')." - " . substr(\implode(' ', $attributeStrings), 0, -2) . " - )") + ->prepare($sql) ->execute(); $this->createIndex($id, '_index1', Database::INDEX_UNIQUE, ['_uid'], [], []); @@ -140,13 +153,19 @@ public function createCollection(string $name, array $attributes = [], array $in $this->createIndex($id, $indexId, $indexType, $indexAttributes, $indexLengths, $indexOrders); } + $sql = " + CREATE TABLE IF NOT EXISTS `{$namespace}_{$id}_perms` ( + `_id` INTEGER PRIMARY KEY AUTOINCREMENT, + `_type` VARCHAR(12) NOT NULL, + `_permission` VARCHAR(255) NOT NULL, + `_document` VARCHAR(255) NOT NULL + ) + "; + + $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + $this->getPDO() - ->prepare("CREATE TABLE IF NOT EXISTS `{$namespace}_{$id}_perms` ( - `_id` INTEGER PRIMARY KEY AUTOINCREMENT, - `_type` VARCHAR(12) NOT NULL, - `_permission` VARCHAR(255) NOT NULL, - `_document` VARCHAR(255) NOT NULL - )") + ->prepare($sql) ->execute(); $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []); @@ -207,12 +226,18 @@ public function deleteCollection(string $id): bool $this->getPDO()->beginTransaction(); + $sql = "DROP TABLE `{$this->getNamespace()}_{$id}`"; + $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); + $this->getPDO() - ->prepare("DROP TABLE `{$this->getNamespace()}_{$id}`;") + ->prepare($sql) ->execute(); + $sql = "DROP TABLE `{$this->getNamespace()}_{$id}_perms`"; + $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); + $this->getPDO() - ->prepare("DROP TABLE `{$this->getNamespace()}_{$id}_perms`;") + ->prepare($sql) ->execute(); $this->getPDO()->commit(); @@ -271,9 +296,15 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa } } + $sql = " + ALTER TABLE {$this->getSQLTable($name)} + DROP COLUMN `{$id}` + "; + + $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); + return $this->getPDO() - ->prepare("ALTER TABLE {$this->getSQLTable($name)} - DROP COLUMN `{$id}`;") + ->prepare($sql) ->execute(); } @@ -337,8 +368,12 @@ public function createIndex(string $collection, string $id, string $type, array $name = $this->filter($collection); $id = $this->filter($id); + $sql = $this->getSQLIndex($name, $id, $type, $attributes); + + $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); + return $this->getPDO() - ->prepare($this->getSQLIndex($name, $id, $type, $attributes)) + ->prepare($sql) ->execute(); } @@ -356,8 +391,11 @@ public function deleteIndex(string $collection, string $id): bool $name = $this->filter($collection); $id = $this->filter($id); + $sql = "DROP INDEX `{$this->getNamespace()}_{$name}_{$id}`"; + $sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql); + return $this->getPDO() - ->prepare("DROP INDEX `{$this->getNamespace()}_{$name}_{$id}`;") + ->prepare($sql) ->execute(); } @@ -401,9 +439,14 @@ public function createDocument(string $collection, Document $document): Document $columns[] = "_id"; } - $stmt = $this->getPDO() - ->prepare("INSERT INTO `{$this->getNamespace()}_{$name}` - (".implode(', ', $columns).") VALUES (:".implode(', :', $values).");"); + $sql = " + INSERT INTO `{$this->getNamespace()}_{$name}` (".\implode(', ', $columns).") + VALUES (:".\implode(', :', $values)."); + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_CREATE, $sql); + + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR); @@ -434,14 +477,21 @@ public function createDocument(string $collection, Document $document): Document } if (!empty($permissions)) { - $queryPermissions = "INSERT INTO `{$this->getNamespace()}_{$name}_perms` (_type, _permission, _document) VALUES " . implode(', ', $permissions); + $queryPermissions = " + INSERT INTO `{$this->getNamespace()}_{$name}_perms` (_type, _permission, _document) + VALUES " . \implode( + ', ', + $permissions + ); + $queryPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $queryPermissions); + $stmtPermissions = $this->getPDO()->prepare($queryPermissions); } try { $stmt->execute(); - $statment = $this->getPDO()->prepare("select last_insert_rowid() as id"); + $statment = $this->getPDO()->prepare("SELECT last_insert_rowid() AS id"); $statment->execute(); $last = $statment->fetch(); $document['$internalId'] = $last['id']; @@ -484,14 +534,19 @@ public function updateDocument(string $collection, Document $document): Document $name = $this->filter($collection); $columns = ''; + + $sql = " + SELECT _type, _permission + FROM `{$this->getNamespace()}_{$name}_perms` + WHERE _document = :_uid + "; + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); + /** * Get current permissions from the database */ - $permissionsStmt = $this->getPDO()->prepare(" - SELECT _type, _permission - FROM `{$this->getNamespace()}_{$name}_perms` p - WHERE p._document = :_uid - "); + $permissionsStmt = $this->getPDO()->prepare($sql); $permissionsStmt->bindValue(':_uid', $document->getId()); $permissionsStmt->execute(); $permissions = $permissionsStmt->fetchAll(); @@ -549,14 +604,16 @@ public function updateDocument(string $collection, Document $document): Document } if (!empty($removeQuery)) { $removeQuery .= ')'; - $stmtRemovePermissions = $this->getPDO() - ->prepare(" - DELETE + $removeQuery = " + DELETE FROM `{$this->getNamespace()}_{$name}_perms` WHERE _document = :_uid {$removeQuery} - "); + "; + $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); + + $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); $stmtRemovePermissions->bindValue(':_uid', $document->getId()); foreach ($removals as $type => $permissions) { @@ -577,12 +634,13 @@ public function updateDocument(string $collection, Document $document): Document } } - $stmtAddPermissions = $this->getPDO() - ->prepare( - " - INSERT INTO `{$this->getNamespace()}_{$name}_perms` - (_document, _type, _permission) VALUES " . \implode(', ', $values) - ); + $sql = " + INSERT INTO `{$this->getNamespace()}_{$name}_perms` + (_document, _type, _permission) VALUES " . \implode(', ', $values); + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql); + + $stmtAddPermissions = $this->getPDO()->prepare($sql); $stmtAddPermissions->bindValue(":_uid", $document->getId()); foreach ($additions as $type => $permissions) { @@ -595,7 +653,6 @@ public function updateDocument(string $collection, Document $document): Document /** * Update Attributes */ - $bindIndex = 0; foreach ($attributes as $attribute => $value) { $column = $this->filter($attribute); @@ -604,9 +661,14 @@ public function updateDocument(string $collection, Document $document): Document $bindIndex++; } - $stmt = $this->getPDO() - ->prepare("UPDATE `{$this->getNamespace()}_{$name}` - SET {$columns} _uid = :_uid WHERE _uid = :_uid"); + $sql = " + UPDATE `{$this->getNamespace()}_{$name}` + SET {$columns} _uid = :_uid WHERE _uid = :_uid + "; + + $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); + + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $document->getId()); diff --git a/src/Database/Database.php b/src/Database/Database.php index 8292bf6bf..f1e71c2cf 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -115,6 +115,10 @@ class Database public const EVENT_DOCUMENT_INCREASE = 'document_increase'; public const EVENT_DOCUMENT_DECREASE = 'document_decrease'; + public const EVENT_PERMISSIONS_CREATE = 'permissions_create'; + public const EVENT_PERMISSIONS_READ = 'permissions_read'; + public const EVENT_PERMISSIONS_DELETE = 'permissions_delete'; + public const EVENT_ATTRIBUTE_CREATE = 'attribute_create'; public const EVENT_ATTRIBUTE_UPDATE = 'attribute_update'; public const EVENT_ATTRIBUTE_DELETE = 'attribute_delete'; @@ -392,6 +396,7 @@ function (?string $value) { * Add listener to events * * @param string $event + * @param string $name * @param callable $callback * @return self */ @@ -401,6 +406,22 @@ public function on(string $event, string $name, callable $callback): self $this->listeners[$event] = []; } $this->listeners[$event][$name] = $callback; + + return $this; + } + + /** + * Add a transformation to be applied to a query string before an event occurs + * + * @param string $event + * @param string $name + * @param callable $callback + * @return $this + */ + public function before(string $event, string $name, callable $callback): self + { + $this->adapter->before($event, $name, $callback); + return $this; } @@ -468,14 +489,14 @@ protected function trigger(string $event, mixed $args = null): void if (isset($this->silentListeners[$name])) { continue; } - call_user_func($callback, $event, $args); + $callback($event, $args); } foreach (($this->listeners[$event] ?? []) as $name => $callback) { if (isset($this->silentListeners[$name])) { continue; } - call_user_func($callback, $event, $args); + $callback($event, $args); } } @@ -559,6 +580,30 @@ public function getDefaultDatabase(): string return $this->adapter->getDefaultDatabase(); } + + /** + * Set maximum query execution time + * + * @param int $milliseconds + * @return void + * @throws Exception + */ + public function setTimeout(int $milliseconds): void + { + $this->adapter->setTimeout($milliseconds); + } + + /** + * Clear maximum query execution time + * + * @return void + * @throws Exception + */ + public function clearTimeout(): void + { + $this->adapter->clearTimeout(); + } + /** * Ping Database * @@ -4295,15 +4340,6 @@ public function sum(string $collection, string $attribute, array $queries = [], return $sum; } - public function setTimeout(int $milliseconds): void - { - $this->adapter->setTimeout($milliseconds); - } - - public function clearTimeout(): void - { - $this->adapter->clearTimeout(); - } /** * Add Attribute Filter * From 110e4eb9bd6b50dc75f59e18eaf9756c56079335 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 10 Oct 2023 22:02:55 +1300 Subject: [PATCH 2/6] Allow setting custom metadata which will be added as query comments --- src/Database/Adapter.php | 30 ++++++++++++++++++++++++++++++ src/Database/Database.php | 18 ++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index ee132c71b..d431eb22d 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -29,6 +29,11 @@ abstract class Adapter '*' => [], ]; + /** + * @var array + */ + protected array $metadata = []; + protected static ?int $timeout = null; /** @@ -142,6 +147,31 @@ public function getDefaultDatabase(): string return $this->defaultDatabase; } + public function setMetadata(string $key, mixed $value): self + { + $this->metadata[$key] = $value; + + $output = ''; + foreach ($this->metadata as $key => $value) { + $output .= "-- {$key}: {$value}"; + if (\array_key_last($this->metadata) !== $key) { + $output .= "\n"; + } + } + + $this->before(Database::EVENT_ALL, 'metadata', function ($query) use ($output) { + return $output . $query; + }); + + return $this; + } + + public function clearMetadata(): self + { + $this->metadata = []; + + return $this; + } /** * Apply a transformation to a query before an event occurs diff --git a/src/Database/Database.php b/src/Database/Database.php index f1e71c2cf..107c6812b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -580,6 +580,24 @@ public function getDefaultDatabase(): string return $this->adapter->getDefaultDatabase(); } + /** + * Set a metadata value to be printed in the query comments + * + * @param string $key + * @param mixed $value + * @return self + */ + public function setMetadata(string $key, mixed $value): self + { + $this->adapter->setMetadata($key, $value); + + return $this; + } + + public function clearMetadata(): void + { + $this->adapter->clearMetadata(); + } /** * Set maximum query execution time From e8c950d8b311ebd55f8bbaf3d1d26c0262cd8a8d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 10 Oct 2023 22:03:04 +1300 Subject: [PATCH 3/6] Add tests --- tests/Database/Base.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 34609d113..ac12ee345 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -12331,6 +12331,15 @@ public function testLabels(): void $this->assertCount(1, $documents); } + public function testMetadata(): void + { + static::getDatabase()->setMetadata('key', 'value'); + + static::getDatabase()->createCollection('testers'); + + $this->expectNotToPerformAssertions(); + } + public function testEmptyOperatorValues(): void { try { @@ -12414,6 +12423,31 @@ public function testEmptyOperatorValues(): void } } + public function testTransformations(): void + { + static::getDatabase()->createCollection('docs', attributes: [ + new Document([ + '$id' => 'name', + 'type' => Database::VAR_STRING, + 'size' => 767, + 'required' => true, + ]) + ]); + + static::getDatabase()->createDocument('docs', new Document([ + '$id' => 'doc1', + 'name' => 'value1', + ])); + + static::getDatabase()->before(Database::EVENT_DOCUMENT_READ, 'test', function (string $query) { + return "SELECT 1"; + }); + + $result = static::getDatabase()->getDocument('docs', 'doc1'); + + $this->assertTrue($result->isEmpty()); + } + public function testEvents(): void { Authorization::skip(function () { From e517a6512339017c8f01ca8c886b2d08c02ff09a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Oct 2023 15:10:43 +1300 Subject: [PATCH 4/6] Always set newline --- src/Database/Adapter.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index d431eb22d..0d5fb80c1 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -153,10 +153,7 @@ public function setMetadata(string $key, mixed $value): self $output = ''; foreach ($this->metadata as $key => $value) { - $output .= "-- {$key}: {$value}"; - if (\array_key_last($this->metadata) !== $key) { - $output .= "\n"; - } + $output .= "-- {$key}: {$value}\n"; } $this->before(Database::EVENT_ALL, 'metadata', function ($query) use ($output) { From d0ba82a4f964df243ee63d44b640205764a47ee2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Oct 2023 15:23:38 +1300 Subject: [PATCH 5/6] Update tests --- src/Database/Adapter.php | 26 ++++++++++++++++++++++++-- src/Database/Database.php | 19 +++++++++++++++++-- tests/Database/Base.php | 7 ++++++- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 0d5fb80c1..b18d4010d 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -147,7 +147,14 @@ public function getDefaultDatabase(): string return $this->defaultDatabase; } - public function setMetadata(string $key, mixed $value): self + /** + * Set metadata for query comments + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function setMetadata(string $key, mixed $value): self { $this->metadata[$key] = $value; @@ -163,7 +170,22 @@ public function setMetadata(string $key, mixed $value): self return $this; } - public function clearMetadata(): self + /** + * Get metadata + * + * @return array + */ + public function getMetadata(): array + { + return $this->metadata; + } + + /** + * Clear existing metadata + * + * @return $this + */ + public function resetMetadata(): self { $this->metadata = []; diff --git a/src/Database/Database.php b/src/Database/Database.php index 107c6812b..d66cc82b8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -594,9 +594,24 @@ public function setMetadata(string $key, mixed $value): self return $this; } - public function clearMetadata(): void + /** + * Get metadata + * + * @return array + */ + public function getMetadata(): array + { + return $this->adapter->getMetadata(); + } + + /** + * Clear metadata + * + * @return void + */ + public function resetMetadata(): void { - $this->adapter->clearMetadata(); + $this->adapter->resetMetadata(); } /** diff --git a/tests/Database/Base.php b/tests/Database/Base.php index ac12ee345..9e1fc026b 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -12337,7 +12337,12 @@ public function testMetadata(): void static::getDatabase()->createCollection('testers'); - $this->expectNotToPerformAssertions(); + $this->assertEquals(['key' => 'value'], static::getDatabase()->getMetadata()); + + static::getDatabase()->resetMetadata(); + + $this->assertEquals([], static::getDatabase()->getMetadata()); + } public function testEmptyOperatorValues(): void From a072b8a3c4c3948083129a0768be9af7a2c224f2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Oct 2023 15:24:28 +1300 Subject: [PATCH 6/6] Format --- src/Database/Adapter.php | 46 +++++++++++++++++++-------------------- src/Database/Database.php | 30 ++++++++++++------------- tests/Database/Base.php | 7 +++--- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index b18d4010d..422b62bd4 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -147,14 +147,14 @@ public function getDefaultDatabase(): string return $this->defaultDatabase; } - /** - * Set metadata for query comments - * - * @param string $key - * @param mixed $value - * @return $this - */ - public function setMetadata(string $key, mixed $value): self + /** + * Set metadata for query comments + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function setMetadata(string $key, mixed $value): self { $this->metadata[$key] = $value; @@ -170,21 +170,21 @@ public function setMetadata(string $key, mixed $value): self return $this; } - /** - * Get metadata - * - * @return array - */ - public function getMetadata(): array - { - return $this->metadata; - } - - /** - * Clear existing metadata - * - * @return $this - */ + /** + * Get metadata + * + * @return array + */ + public function getMetadata(): array + { + return $this->metadata; + } + + /** + * Clear existing metadata + * + * @return $this + */ public function resetMetadata(): self { $this->metadata = []; diff --git a/src/Database/Database.php b/src/Database/Database.php index d66cc82b8..28c1fc8ed 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -594,21 +594,21 @@ public function setMetadata(string $key, mixed $value): self return $this; } - /** - * Get metadata - * - * @return array - */ - public function getMetadata(): array - { - return $this->adapter->getMetadata(); - } - - /** - * Clear metadata - * - * @return void - */ + /** + * Get metadata + * + * @return array + */ + public function getMetadata(): array + { + return $this->adapter->getMetadata(); + } + + /** + * Clear metadata + * + * @return void + */ public function resetMetadata(): void { $this->adapter->resetMetadata(); diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 9e1fc026b..5661079a5 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -12337,12 +12337,11 @@ public function testMetadata(): void static::getDatabase()->createCollection('testers'); - $this->assertEquals(['key' => 'value'], static::getDatabase()->getMetadata()); + $this->assertEquals(['key' => 'value'], static::getDatabase()->getMetadata()); - static::getDatabase()->resetMetadata(); - - $this->assertEquals([], static::getDatabase()->getMetadata()); + static::getDatabase()->resetMetadata(); + $this->assertEquals([], static::getDatabase()->getMetadata()); } public function testEmptyOperatorValues(): void