55use Exception ;
66use MongoDB \BSON \Regex ;
77use MongoDB \BSON \UTCDateTime ;
8+ use stdClass ;
89use Utopia \Database \Adapter ;
910use Utopia \Database \Change ;
1011use Utopia \Database \Database ;
@@ -43,6 +44,8 @@ class Mongo extends Adapter
4344 '$not ' ,
4445 '$nor ' ,
4546 '$exists ' ,
47+ '$elemMatch ' ,
48+ '$exists '
4649 ];
4750
4851 protected Client $ client ;
@@ -415,7 +418,6 @@ public function createCollection(string $name, array $attributes = [], array $in
415418 try {
416419 $ options = $ this ->getTransactionOptions ();
417420 $ this ->getClient ()->createCollection ($ id , $ options );
418-
419421 } catch (MongoException $ e ) {
420422 $ e = $ this ->processException ($ e );
421423 if ($ e instanceof DuplicateException) {
@@ -1232,7 +1234,7 @@ public function castingAfter(Document $collection, Document $document): Document
12321234 case Database::VAR_INTEGER :
12331235 $ node = (int )$ node ;
12341236 break ;
1235- case Database::VAR_DATETIME :
1237+ case Database::VAR_DATETIME :
12361238 if ($ node instanceof UTCDateTime) {
12371239 // Handle UTCDateTime objects
12381240 $ node = DateTime::format ($ node ->toDateTime ());
@@ -1258,6 +1260,12 @@ public function castingAfter(Document $collection, Document $document): Document
12581260 }
12591261 }
12601262 break ;
1263+ case Database::VAR_OBJECT :
1264+ // Convert stdClass objects to arrays for object attributes
1265+ if (is_object ($ node ) && get_class ($ node ) === stdClass::class) {
1266+ $ node = $ this ->convertStdClassToArray ($ node );
1267+ }
1268+ break ;
12611269 default :
12621270 break ;
12631271 }
@@ -1266,9 +1274,33 @@ public function castingAfter(Document $collection, Document $document): Document
12661274 $ document ->setAttribute ($ key , ($ array ) ? $ value : $ value [0 ]);
12671275 }
12681276
1277+ if (!$ this ->getSupportForAttributes ()) {
1278+ foreach ($ document ->getArrayCopy () as $ key => $ value ) {
1279+ // mongodb results out a stdclass for objects
1280+ if (is_object ($ value ) && get_class ($ value ) === stdClass::class) {
1281+ $ document ->setAttribute ($ key , $ this ->convertStdClassToArray ($ value ));
1282+ }
1283+ }
1284+ }
12691285 return $ document ;
12701286 }
12711287
1288+ private function convertStdClassToArray (mixed $ value ): mixed
1289+ {
1290+ if (is_object ($ value ) && get_class ($ value ) === stdClass::class) {
1291+ return array_map ($ this ->convertStdClassToArray (...), get_object_vars ($ value ));
1292+ }
1293+
1294+ if (is_array ($ value )) {
1295+ return array_map (
1296+ fn ($ v ) => $ this ->convertStdClassToArray ($ v ),
1297+ $ value
1298+ );
1299+ }
1300+
1301+ return $ value ;
1302+ }
1303+
12721304 /**
12731305 * Returns the document after casting to
12741306 * @param Document $collection
@@ -1319,6 +1351,9 @@ public function castingBefore(Document $collection, Document $document): Documen
13191351 $ node = new UTCDateTime (new \DateTime ($ node ));
13201352 }
13211353 break ;
1354+ case Database::VAR_OBJECT :
1355+ $ node = json_decode ($ node );
1356+ break ;
13221357 default :
13231358 break ;
13241359 }
@@ -1592,7 +1627,6 @@ public function upsertDocuments(Document $collection, string $attribute, array $
15921627 $ operations ,
15931628 options: $ options
15941629 );
1595-
15961630 } catch (MongoException $ e ) {
15971631 throw $ this ->processException ($ e );
15981632 }
@@ -1977,7 +2011,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
19772011 // Process first batch
19782012 foreach ($ results as $ result ) {
19792013 $ record = $ this ->replaceChars ('_ ' , '$ ' , (array )$ result );
1980- $ found [] = new Document ($ record );
2014+ $ found [] = new Document ($ this -> convertStdClassToArray ( $ record) );
19812015 }
19822016
19832017 // Get cursor ID for subsequent batches
@@ -1999,7 +2033,6 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
19992033
20002034 $ cursorId = (int )($ moreResponse ->cursor ->id ?? 0 );
20012035 }
2002-
20032036 } catch (MongoException $ e ) {
20042037 throw $ this ->processException ($ e );
20052038 } finally {
@@ -2335,6 +2368,15 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr
23352368 foreach ($ queries as $ query ) {
23362369 /* @var $query Query */
23372370 if ($ query ->isNested ()) {
2371+ if ($ query ->getMethod () === Query::TYPE_ELEM_MATCH ) {
2372+ $ filters [$ separator ][] = [
2373+ $ query ->getAttribute () => [
2374+ '$elemMatch ' => $ this ->buildFilters ($ query ->getValues (), $ separator )
2375+ ]
2376+ ];
2377+ continue ;
2378+ }
2379+
23382380 $ operator = $ this ->getQueryOperator ($ query ->getMethod ());
23392381
23402382 $ filters [$ separator ][] = $ this ->buildFilters ($ query ->getValues (), $ operator );
@@ -2385,6 +2427,10 @@ protected function buildFilter(Query $query): array
23852427 };
23862428
23872429 $ filter = [];
2430+ if ($ query ->isObjectAttribute () && in_array ($ query ->getMethod (), [Query::TYPE_EQUAL , Query::TYPE_CONTAINS , Query::TYPE_NOT_CONTAINS , Query::TYPE_NOT_EQUAL ])) {
2431+ $ this ->handleObjectFilters ($ query , $ filter );
2432+ return $ filter ;
2433+ }
23882434
23892435 if ($ operator == '$eq ' && \is_array ($ value )) {
23902436 $ filter [$ attribute ]['$in ' ] = $ value ;
@@ -2448,6 +2494,88 @@ protected function buildFilter(Query $query): array
24482494 return $ filter ;
24492495 }
24502496
2497+ /**
2498+ * @param Query $query
2499+ * @param array<string, mixed> $filter
2500+ * @return void
2501+ */
2502+ private function handleObjectFilters (Query $ query , array &$ filter ): void
2503+ {
2504+ $ conditions = [];
2505+ $ isNot = in_array ($ query ->getMethod (), [Query::TYPE_NOT_CONTAINS ,Query::TYPE_NOT_EQUAL ]);
2506+ $ values = $ query ->getValues ();
2507+ foreach ($ values as $ attribute => $ value ) {
2508+ $ flattendQuery = $ this ->flattenWithDotNotation (is_string ($ attribute ) ? $ attribute : '' , $ value );
2509+ $ flattenedObjectKey = array_key_first ($ flattendQuery );
2510+ $ queryValue = $ flattendQuery [$ flattenedObjectKey ];
2511+ $ flattenedObjectKey = $ query ->getAttribute () . '. ' . array_key_first ($ flattendQuery );
2512+ switch ($ query ->getMethod ()) {
2513+
2514+ case Query::TYPE_CONTAINS :
2515+ case Query::TYPE_NOT_CONTAINS : {
2516+ $ arrayValue = \is_array ($ queryValue ) ? $ queryValue : [$ queryValue ];
2517+ $ operator = $ isNot ? '$nin ' : '$in ' ;
2518+ $ conditions [] = [ $ flattenedObjectKey => [ $ operator => $ arrayValue ] ];
2519+ break ;
2520+ }
2521+
2522+ case Query::TYPE_EQUAL :
2523+ case Query::TYPE_NOT_EQUAL : {
2524+ if (\is_array ($ queryValue )) {
2525+ $ operator = $ isNot ? '$nin ' : '$in ' ;
2526+ $ conditions [] = [ $ flattenedObjectKey => [ $ operator => $ queryValue ] ];
2527+ } else {
2528+ $ operator = $ isNot ? '$ne ' : '$eq ' ;
2529+ $ conditions [] = [ $ flattenedObjectKey => [ $ operator => $ queryValue ] ];
2530+ }
2531+
2532+ break ;
2533+ }
2534+ }
2535+ }
2536+
2537+ $ logicalOperator = $ isNot ? '$and ' : '$or ' ;
2538+ if (count ($ conditions ) && isset ($ filter [$ logicalOperator ])) {
2539+ $ filter [$ logicalOperator ] = array_merge ($ filter [$ logicalOperator ], $ conditions );
2540+ } else {
2541+ $ filter [$ logicalOperator ] = $ conditions ;
2542+ }
2543+ }
2544+
2545+ /**
2546+ * Flatten a nested associative array into Mongo-style dot notation.
2547+ *
2548+ * @param string $key
2549+ * @param mixed $value
2550+ * @param string $prefix
2551+ * @return array<string, mixed>
2552+ */
2553+ private function flattenWithDotNotation (string $ key , mixed $ value , string $ prefix = '' ): array
2554+ {
2555+ /** @var array<string, mixed> $result */
2556+ $ result = [];
2557+
2558+ $ stack = [];
2559+
2560+ $ initialKey = $ prefix === '' ? $ key : $ prefix . '. ' . $ key ;
2561+ $ stack [] = [$ initialKey , $ value ];
2562+ while (!empty ($ stack )) {
2563+ [$ currentPath , $ currentValue ] = array_pop ($ stack );
2564+ if (is_array ($ currentValue ) && !array_is_list ($ currentValue )) {
2565+ foreach ($ currentValue as $ nextKey => $ nextValue ) {
2566+ $ nextKey = (string )$ nextKey ;
2567+ $ nextPath = $ currentPath === '' ? $ nextKey : $ currentPath . '. ' . $ nextKey ;
2568+ $ stack [] = [$ nextPath , $ nextValue ];
2569+ }
2570+ } else {
2571+ // leaf node
2572+ $ result [$ currentPath ] = $ currentValue ;
2573+ }
2574+ }
2575+
2576+ return $ result ;
2577+ }
2578+
24512579 /**
24522580 * Get Query Operator
24532581 *
@@ -2482,6 +2610,7 @@ protected function getQueryOperator(string $operator): string
24822610 Query::TYPE_AND => '$and ' ,
24832611 Query::TYPE_EXISTS ,
24842612 Query::TYPE_NOT_EXISTS => '$exists ' ,
2613+ Query::TYPE_ELEM_MATCH => '$elemMatch ' ,
24852614 default => throw new DatabaseException ('Unknown operator: ' . $ operator . '. Must be one of ' . Query::TYPE_EQUAL . ', ' . Query::TYPE_NOT_EQUAL . ', ' . Query::TYPE_LESSER . ', ' . Query::TYPE_LESSER_EQUAL . ', ' . Query::TYPE_GREATER . ', ' . Query::TYPE_GREATER_EQUAL . ', ' . Query::TYPE_IS_NULL . ', ' . Query::TYPE_IS_NOT_NULL . ', ' . Query::TYPE_BETWEEN . ', ' . Query::TYPE_NOT_BETWEEN . ', ' . Query::TYPE_STARTS_WITH . ', ' . Query::TYPE_NOT_STARTS_WITH . ', ' . Query::TYPE_ENDS_WITH . ', ' . Query::TYPE_NOT_ENDS_WITH . ', ' . Query::TYPE_CONTAINS . ', ' . Query::TYPE_NOT_CONTAINS . ', ' . Query::TYPE_SEARCH . ', ' . Query::TYPE_NOT_SEARCH . ', ' . Query::TYPE_SELECT ),
24862615 };
24872616 }
@@ -2821,6 +2950,16 @@ public function getSupportForBatchCreateAttributes(): bool
28212950 }
28222951
28232952 public function getSupportForObject (): bool
2953+ {
2954+ return true ;
2955+ }
2956+
2957+ /**
2958+ * Are object (JSON) indexes supported?
2959+ *
2960+ * @return bool
2961+ */
2962+ public function getSupportForObjectIndexes (): bool
28242963 {
28252964 return false ;
28262965 }
0 commit comments