diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 3ffc52da1..70aa70c67 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1046,17 +1046,26 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $results = $stmt->fetchAll(); foreach ($results as $key => $value) { - $results[$key]['$id'] = $value['_uid']; - $results[$key]['$internalId'] = $value['_id']; - $results[$key]['$createdAt'] = $value['_createdAt']; - $results[$key]['$updatedAt'] = $value['_updatedAt']; - $results[$key]['$permissions'] = json_decode($value['_permissions'] ?? '[]', true); - - unset($results[$key]['_uid']); - unset($results[$key]['_id']); - unset($results[$key]['_createdAt']); - unset($results[$key]['_updatedAt']); - unset($results[$key]['_permissions']); + if (isset($value['_uid'])) { + $results[$key]['$id'] = $value['_uid']; + unset($results[$key]['_uid']); + } + if (isset($value['_id'])) { + $results[$key]['$internalId'] = $value['_id']; + unset($results[$key]['_id']); + } + if (isset($value['_createdAt'])) { + $results[$key]['$createdAt'] = $value['_createdAt']; + unset($results[$key]['_createdAt']); + } + if (isset($value['_updatedAt'])) { + $results[$key]['$updatedAt'] = $value['_updatedAt']; + unset($results[$key]['_updatedAt']); + } + if (isset($value['_permissions'])) { + $results[$key]['$permissions'] = json_decode($value['_permissions'] ?? '[]', true); + unset($results[$key]['_permissions']); + } $results[$key] = new Document($results[$key]); } @@ -1189,12 +1198,25 @@ protected function getAttributeProjection(array $selections, string $prefix = '' return '*'; } + // Remove $id, $permissions and $collection if present since it is always selected by default + $selections = \array_diff($selections, ['$id', '$permissions', '$collection']); + $selections[] = '_uid'; - $selections[] = '_id'; - $selections[] = '_createdAt'; - $selections[] = '_updatedAt'; $selections[] = '_permissions'; + if (\in_array('$internalId', $selections)) { + $selections[] = '_id'; + $selections = \array_diff($selections, ['$internalId']); + } + if (\in_array('$createdAt', $selections)) { + $selections[] = '_createdAt'; + $selections = \array_diff($selections, ['$createdAt']); + } + if (\in_array('$updatedAt', $selections)) { + $selections[] = '_updatedAt'; + $selections = \array_diff($selections, ['$updatedAt']); + } + if (!empty($prefix)) { foreach ($selections as &$selection) { $selection = "`{$prefix}`.`{$this->filter($selection)}`"; diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index a68440e16..f7b827a82 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1327,6 +1327,11 @@ protected function getAttributeProjection(array $selections, string $prefix = '' $projection = []; foreach ($selections as $selection) { + // Skip internal attributes since all are selected by default + if (\in_array($selection, Database::INTERNAL_ATTRIBUTES)) { + continue; + } + $projection[$selection] = 1; } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 348a2bd51..aa0f8f5ad 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1056,17 +1056,26 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $results = $stmt->fetchAll(); foreach ($results as $key => $value) { - $results[$key]['$id'] = $value['_uid']; - $results[$key]['$internalId'] = $value['_id']; - $results[$key]['$createdAt'] = $value['_createdAt']; - $results[$key]['$updatedAt'] = $value['_updatedAt']; - $results[$key]['$permissions'] = json_decode($value['_permissions'] ?? '[]', true); - - unset($results[$key]['_uid']); - unset($results[$key]['_id']); - unset($results[$key]['_createdAt']); - unset($results[$key]['_updatedAt']); - unset($results[$key]['_permissions']); + if (isset($value['_uid'])) { + $results[$key]['$id'] = $value['_uid']; + unset($results[$key]['_uid']); + } + if (isset($value['_id'])) { + $results[$key]['$internalId'] = $value['_id']; + unset($results[$key]['_id']); + } + if (isset($value['_createdAt'])) { + $results[$key]['$createdAt'] = $value['_createdAt']; + unset($results[$key]['_createdAt']); + } + if (isset($value['_updatedAt'])) { + $results[$key]['$updatedAt'] = $value['_updatedAt']; + unset($results[$key]['_updatedAt']); + } + if (isset($value['_permissions'])) { + $results[$key]['$permissions'] = json_decode($value['_permissions'] ?? '[]', true); + unset($results[$key]['_permissions']); + } $results[$key] = new Document($results[$key]); } @@ -1199,12 +1208,25 @@ protected function getAttributeProjection(array $selections, string $prefix = '' return '*'; } + // Remove $id ,$permissions and $collection from selections if present since they are always selected + $selections = \array_diff($selections, ['$id', '$permissions', '$collection']); + $selections[] = '_uid'; - $selections[] = '_id'; - $selections[] = '_createdAt'; - $selections[] = '_updatedAt'; $selections[] = '_permissions'; + if (\in_array('$internalId', $selections)) { + $selections[] = '_id'; + $selections = \array_diff($selections, ['$internalId']); + } + if (\in_array('$createdAt', $selections)) { + $selections[] = '_createdAt'; + $selections = \array_diff($selections, ['$createdAt']); + } + if (\in_array('$updatedAt', $selections)) { + $selections[] = '_updatedAt'; + $selections = \array_diff($selections, ['$updatedAt']); + } + if (!empty($prefix)) { foreach ($selections as &$selection) { $selection = "\"{$prefix}\".\"{$this->filter($selection)}\""; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 763986d5d..50de353ef 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -127,17 +127,26 @@ public function getDocument(string $collection, string $id, array $queries = []) return new Document([]); } - $document['$id'] = $document['_uid']; - $document['$internalId'] = $document['_id']; - $document['$createdAt'] = $document['_createdAt']; - $document['$updatedAt'] = $document['_updatedAt']; - $document['$permissions'] = json_decode($document['_permissions'] ?? '[]', true); - - unset($document['_id']); - unset($document['_uid']); - unset($document['_createdAt']); - unset($document['_updatedAt']); - unset($document['_permissions']); + if (isset($document['_id'])) { + $document['$internalId'] = $document['_id']; + unset($document['_id']); + } + if (isset($document['_uid'])) { + $document['$id'] = $document['_uid']; + unset($document['_uid']); + } + if (isset($document['_createdAt'])) { + $document['$createdAt'] = $document['_createdAt']; + unset($document['_createdAt']); + } + if (isset($document['_updatedAt'])) { + $document['$updatedAt'] = $document['_updatedAt']; + unset($document['_updatedAt']); + } + if (isset($document['_permissions'])) { + $document['$permissions'] = json_decode($document['_permissions'] ?? '[]', true); + unset($document['_permissions']); + } return new Document($document); } diff --git a/src/Database/Database.php b/src/Database/Database.php index c90d68c6b..0f1b46d38 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -187,6 +187,20 @@ class Database ] ]; + /** + * List of Internal Attributes + * + * @var array + */ + public const INTERNAL_ATTRIBUTES = [ + '$id', + '$internalId', + '$createdAt', + '$updatedAt', + '$permissions', + '$collection', + ]; + /** * Parent Collection * Defines the structure for both system and custom collections @@ -2284,6 +2298,20 @@ public function getDocument(string $collection, string $id, array $queries = []) $this->cache->save($cacheKey, $document->getArrayCopy()); } + // Remove internal attributes if not queried for select query + // $id, $permissions and $collection are the default selected attributes for (MariaDB, MySQL, SQLite, Postgres) + // All internal attributes are default selected attributes for (MongoDB) + foreach ($queries as $query) { + if ($query->getMethod() === Query::TYPE_SELECT) { + $values = $query->getValues(); + foreach (Database::INTERNAL_ATTRIBUTES as $internalAttribute) { + if (!in_array($internalAttribute, $values)) { + $document->removeAttribute($internalAttribute); + } + } + } + } + $this->trigger(self::EVENT_DOCUMENT_READ, $document); return $document; @@ -4015,6 +4043,20 @@ public function find(string $collection, array $queries = [], ?int $timeout = nu $results = $this->applyNestedQueries($results, $nestedQueries, $relationships); + // Remove internal attributes which are not queried + foreach ($queries as $query) { + if ($query->getMethod() === Query::TYPE_SELECT) { + $values = $query->getValues(); + foreach ($results as $result) { + foreach (Database::INTERNAL_ATTRIBUTES as $internalAttribute) { + if (!\in_array($internalAttribute, $values)) { + $result->removeAttribute($internalAttribute); + } + } + } + } + } + $this->trigger(self::EVENT_DOCUMENT_FIND, $results); return $results; @@ -4352,7 +4394,19 @@ public function decode(Document $collection, Document $document, array $selectio } if (empty($selections) || \in_array($key, $selections) || \in_array('*', $selections)) { - $document->setAttribute($key, ($array) ? $value : $value[0]); + if ( + empty($selections) + || \in_array($key, $selections) + || \in_array('*', $selections) + || \in_array($key, ['$createdAt', '$updatedAt']) + ) { + // Prevent null values being set for createdAt and updatedAt + if (\in_array($key, ['$createdAt', '$updatedAt']) && $value[0] === null) { + continue; + } else { + $document->setAttribute($key, ($array) ? $value : $value[0]); + } + } } } @@ -4504,6 +4558,10 @@ private function validateSelections(Document $collection, array $queries): array } $keys = []; + + // Allow querying internal attributes + $keys = array_merge($keys, self::INTERNAL_ATTRIBUTES); + foreach ($collection->getAttribute('attributes', []) as $attribute) { if ($attribute['type'] !== self::VAR_RELATIONSHIP) { $keys[] = $attribute['key']; diff --git a/src/Database/Validator/Query/Select.php b/src/Database/Validator/Query/Select.php index de20fc138..833284c5e 100644 --- a/src/Database/Validator/Query/Select.php +++ b/src/Database/Validator/Query/Select.php @@ -12,6 +12,20 @@ class Select extends Base */ protected array $schema = []; + /** + * List of internal attributes + * + * @var array + */ + protected const INTERNAL_ATTRIBUTES = [ + '$id', + '$internalId', + '$createdAt', + '$updatedAt', + '$permissions', + '$collection', + ]; + /** * @param array $attributes */ @@ -54,6 +68,11 @@ public function isValid($value): bool $attribute = \explode('.', $attribute)[0]; } + // Skip internal attributes + if (\in_array($attribute, self::INTERNAL_ATTRIBUTES)) { + continue; + } + if (!isset($this->schema[$attribute]) && $attribute !== '*') { $this->message = 'Attribute not found in schema: ' . $attribute; return false; diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 8a83b39ba..54a67af65 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -1017,7 +1017,9 @@ public function testGetDocument(Document $document): Document */ public function testGetDocumentSelect(Document $document): Document { - $document = static::getDatabase()->getDocument('documents', $document->getId(), [ + $documentId = $document->getId(); + + $document = static::getDatabase()->getDocument('documents', $documentId, [ Query::select(['string', 'integer']), ]); @@ -1030,6 +1032,78 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayNotHasKey('boolean', $document->getAttributes()); $this->assertArrayNotHasKey('colors', $document->getAttributes()); $this->assertArrayNotHasKey('with-dash', $document->getAttributes()); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + $this->assertArrayNotHasKey('$collection', $document); + + $document = static::getDatabase()->getDocument('documents', $documentId, [ + Query::select(['string', 'integer', '$id']), + ]); + + $this->assertArrayHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + $this->assertArrayNotHasKey('$collection', $document); + + $document = static::getDatabase()->getDocument('documents', $documentId, [ + Query::select(['string', 'integer', '$permissions']), + ]); + + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayNotHasKey('$collection', $document); + + $document = static::getDatabase()->getDocument('documents', $documentId, [ + Query::select(['string', 'integer', '$internalId']), + ]); + + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + $this->assertArrayNotHasKey('$collection', $document); + + $document = static::getDatabase()->getDocument('documents', $documentId, [ + Query::select(['string', 'integer', '$collection']), + ]); + + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); + + $document = static::getDatabase()->getDocument('documents', $documentId, [ + Query::select(['string', 'integer', '$createdAt']), + ]); + + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + $this->assertArrayNotHasKey('$collection', $document); + + $document = static::getDatabase()->getDocument('documents', $documentId, [ + Query::select(['string', 'integer', '$updatedAt']), + ]); + + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + $this->assertArrayNotHasKey('$collection', $document); return $document; } @@ -2540,18 +2614,127 @@ public function testFindSelect(): void $documents = static::getDatabase()->find('movies', [ Query::select(['name', 'year']) ]); + foreach ($documents as $document) { + $this->assertArrayHasKey('name', $document); + $this->assertArrayHasKey('year', $document); + $this->assertArrayNotHasKey('director', $document); + $this->assertArrayNotHasKey('price', $document); + $this->assertArrayNotHasKey('active', $document); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + } + + $documents = static::getDatabase()->find('movies', [ + Query::select(['name', 'year', '$id']) + ]); + + foreach ($documents as $document) { + $this->assertArrayHasKey('name', $document); + $this->assertArrayHasKey('year', $document); + $this->assertArrayNotHasKey('director', $document); + $this->assertArrayNotHasKey('price', $document); + $this->assertArrayNotHasKey('active', $document); $this->assertArrayHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + } + + $documents = static::getDatabase()->find('movies', [ + Query::select(['name', 'year', '$internalId']) + ]); + + foreach ($documents as $document) { + $this->assertArrayHasKey('name', $document); + $this->assertArrayHasKey('year', $document); + $this->assertArrayNotHasKey('director', $document); + $this->assertArrayNotHasKey('price', $document); + $this->assertArrayNotHasKey('active', $document); + $this->assertArrayNotHasKey('$id', $document); $this->assertArrayHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + } + + $documents = static::getDatabase()->find('movies', [ + Query::select(['name', 'year', '$collection']) + ]); + + foreach ($documents as $document) { + $this->assertArrayHasKey('name', $document); + $this->assertArrayHasKey('year', $document); + $this->assertArrayNotHasKey('director', $document); + $this->assertArrayNotHasKey('price', $document); + $this->assertArrayNotHasKey('active', $document); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); $this->assertArrayHasKey('$collection', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + } + + $documents = static::getDatabase()->find('movies', [ + Query::select(['name', 'year', '$createdAt']) + ]); + + foreach ($documents as $document) { + $this->assertArrayHasKey('name', $document); + $this->assertArrayHasKey('year', $document); + $this->assertArrayNotHasKey('director', $document); + $this->assertArrayNotHasKey('price', $document); + $this->assertArrayNotHasKey('active', $document); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$collection', $document); $this->assertArrayHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayNotHasKey('$permissions', $document); + } + + $documents = static::getDatabase()->find('movies', [ + Query::select(['name', 'year', '$updatedAt']) + ]); + + foreach ($documents as $document) { + $this->assertArrayHasKey('name', $document); + $this->assertArrayHasKey('year', $document); + $this->assertArrayNotHasKey('director', $document); + $this->assertArrayNotHasKey('price', $document); + $this->assertArrayNotHasKey('active', $document); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayHasKey('$updatedAt', $document); - $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayNotHasKey('$permissions', $document); + } + + $documents = static::getDatabase()->find('movies', [ + Query::select(['name', 'year', '$permissions']) + ]); + + foreach ($documents as $document) { $this->assertArrayHasKey('name', $document); $this->assertArrayHasKey('year', $document); $this->assertArrayNotHasKey('director', $document); $this->assertArrayNotHasKey('price', $document); $this->assertArrayNotHasKey('active', $document); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayNotHasKey('$updatedAt', $document); + $this->assertArrayHasKey('$permissions', $document); } } @@ -4184,7 +4367,7 @@ public function testOneToOneOneWayRelationship(): void $this->assertArrayNotHasKey('area', $person->getAttribute('library')); $person = static::getDatabase()->getDocument('person', 'person1', [ - Query::select(['*', 'library.name']) + Query::select(['*', 'library.name', '$id']) ]); $this->assertEquals('Library 1', $person->getAttribute('library')->getAttribute('name')); @@ -7464,6 +7647,103 @@ public function testSelectRelationshipAttributes(): void $this->assertEquals('Focus', $make['models'][1]['name']); $this->assertArrayNotHasKey('year', $make['models'][0]); $this->assertArrayNotHasKey('year', $make['models'][1]); + $this->assertArrayNotHasKey('$id', $make); + $this->assertArrayNotHasKey('$internalId', $make); + $this->assertArrayNotHasKey('$permissions', $make); + $this->assertArrayNotHasKey('$collection', $make); + $this->assertArrayNotHasKey('$createdAt', $make); + $this->assertArrayNotHasKey('$updatedAt', $make); + + // Select internal attributes + $make = static::getDatabase()->findOne('make', [ + Query::select(['name', '$id']), + ]); + + if (!$make instanceof Document) { + throw new Exception('Make not found'); + } + + $this->assertArrayHasKey('$id', $make); + $this->assertArrayNotHasKey('$internalId', $make); + $this->assertArrayNotHasKey('$collection', $make); + $this->assertArrayNotHasKey('$createdAt', $make); + $this->assertArrayNotHasKey('$updatedAt', $make); + $this->assertArrayNotHasKey('$permissions', $make); + + $make = static::getDatabase()->findOne('make', [ + Query::select(['name', '$internalId']), + ]); + + if (!$make instanceof Document) { + throw new Exception('Make not found'); + } + + $this->assertArrayNotHasKey('$id', $make); + $this->assertArrayHasKey('$internalId', $make); + $this->assertArrayNotHasKey('$collection', $make); + $this->assertArrayNotHasKey('$createdAt', $make); + $this->assertArrayNotHasKey('$updatedAt', $make); + $this->assertArrayNotHasKey('$permissions', $make); + + $make = static::getDatabase()->findOne('make', [ + Query::select(['name', '$collection']), + ]); + + if (!$make instanceof Document) { + throw new Exception('Make not found'); + } + + $this->assertArrayNotHasKey('$id', $make); + $this->assertArrayNotHasKey('$internalId', $make); + $this->assertArrayHasKey('$collection', $make); + $this->assertArrayNotHasKey('$createdAt', $make); + $this->assertArrayNotHasKey('$updatedAt', $make); + $this->assertArrayNotHasKey('$permissions', $make); + + $make = static::getDatabase()->findOne('make', [ + Query::select(['name', '$createdAt']), + ]); + + if (!$make instanceof Document) { + throw new Exception('Make not found'); + } + + $this->assertArrayNotHasKey('$id', $make); + $this->assertArrayNotHasKey('$internalId', $make); + $this->assertArrayNotHasKey('$collection', $make); + $this->assertArrayHasKey('$createdAt', $make); + $this->assertArrayNotHasKey('$updatedAt', $make); + $this->assertArrayNotHasKey('$permissions', $make); + + $make = static::getDatabase()->findOne('make', [ + Query::select(['name', '$updatedAt']), + ]); + + if (!$make instanceof Document) { + throw new Exception('Make not found'); + } + + $this->assertArrayNotHasKey('$id', $make); + $this->assertArrayNotHasKey('$internalId', $make); + $this->assertArrayNotHasKey('$collection', $make); + $this->assertArrayNotHasKey('$createdAt', $make); + $this->assertArrayHasKey('$updatedAt', $make); + $this->assertArrayNotHasKey('$permissions', $make); + + $make = static::getDatabase()->findOne('make', [ + Query::select(['name', '$permissions']), + ]); + + if (!$make instanceof Document) { + throw new Exception('Make not found'); + } + + $this->assertArrayNotHasKey('$id', $make); + $this->assertArrayNotHasKey('$internalId', $make); + $this->assertArrayNotHasKey('$collection', $make); + $this->assertArrayNotHasKey('$createdAt', $make); + $this->assertArrayNotHasKey('$updatedAt', $make); + $this->assertArrayHasKey('$permissions', $make); // Select all parent attributes, some child attributes $make = static::getDatabase()->findOne('make', [