Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -5205,6 +5205,11 @@ public function updateDocument(string $collection, string $id, Document $documen
break;
}

if (Operator::isOperator($value)) {
$shouldUpdate = true;
break;
}

if (!\is_array($value) || !\array_is_list($value)) {
throw new RelationshipException('Invalid relationship value. Must be either an array of documents or document IDs, ' . \gettype($value) . ' given.');
}
Expand Down Expand Up @@ -5610,6 +5615,24 @@ private function updateDocumentRelationships(Document $collection, Document $old
$twoWayKey = (string)$relationship['options']['twoWayKey'];
$side = (string)$relationship['options']['side'];

if (Operator::isOperator($value)) {
$operator = $value;
if ($operator->isArrayOperation()) {
$existingIds = [];
if (\is_array($oldValue)) {
$existingIds = \array_map(function ($item) {
if ($item instanceof Document) {
return $item->getId();
}
return $item;
}, $oldValue);
}

$value = $this->applyRelationshipOperator($operator, $existingIds);
$document->setAttribute($key, $value);
}
}

if ($oldValue == $value) {
if (
($relationType === Database::RELATION_ONE_TO_ONE
Expand Down Expand Up @@ -5969,6 +5992,63 @@ private function getJunctionCollection(Document $collection, Document $relatedCo
: '_' . $relatedCollection->getSequence() . '_' . $collection->getSequence();
}

/**
* Apply an operator to a relationship array of IDs
*
* @param Operator $operator
* @param array<string> $existingIds
* @return array<string|Document>
*/
private function applyRelationshipOperator(Operator $operator, array $existingIds): array
{
$method = $operator->getMethod();
$values = $operator->getValues();

// Extract IDs from operator values (could be strings or Documents)
$valueIds = \array_filter(\array_map(fn ($item) => $item instanceof Document ? $item->getId() : (\is_string($item) ? $item : null), $values));

switch ($method) {
case Operator::TYPE_ARRAY_APPEND:
return \array_values(\array_merge($existingIds, $valueIds));

case Operator::TYPE_ARRAY_PREPEND:
return \array_values(\array_merge($valueIds, $existingIds));

case Operator::TYPE_ARRAY_INSERT:
$index = $values[0] ?? 0;
$item = $values[1] ?? null;
$itemId = $item instanceof Document ? $item->getId() : (\is_string($item) ? $item : null);
if ($itemId !== null) {
\array_splice($existingIds, $index, 0, [$itemId]);
}
return \array_values($existingIds);

case Operator::TYPE_ARRAY_REMOVE:
$toRemove = $values[0] ?? null;
if (\is_array($toRemove)) {
$toRemoveIds = \array_filter(\array_map(fn ($item) => $item instanceof Document ? $item->getId() : (\is_string($item) ? $item : null), $toRemove));
return \array_values(\array_diff($existingIds, $toRemoveIds));
}
$toRemoveId = $toRemove instanceof Document ? $toRemove->getId() : (\is_string($toRemove) ? $toRemove : null);
if ($toRemoveId !== null) {
return \array_values(\array_diff($existingIds, [$toRemoveId]));
}
return $existingIds;

case Operator::TYPE_ARRAY_UNIQUE:
return \array_values(\array_unique($existingIds));

case Operator::TYPE_ARRAY_INTERSECT:
return \array_values(\array_intersect($existingIds, $valueIds));

case Operator::TYPE_ARRAY_DIFF:
return \array_values(\array_diff($existingIds, $valueIds));

default:
return $existingIds;
}
}

/**
* Create or update a document.
*
Expand Down
130 changes: 123 additions & 7 deletions src/Database/Validator/Operator.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,50 @@ public function __construct(Document $collection, ?Document $currentDocument = n
}
}

/**
* Check if a value is a valid relationship reference (string ID or Document)
*
* @param mixed $item
* @return bool
*/
private function isValidRelationshipValue(mixed $item): bool
{
return \is_string($item) || $item instanceof Document;
}

/**
* Check if a relationship attribute represents a "many" side (returns array of documents)
*
* @param Document|array<string, mixed> $attribute
* @return bool
*/
private function isRelationshipArray(Document|array $attribute): bool
{
$options = $attribute instanceof Document
? $attribute->getAttribute('options', [])
: ($attribute['options'] ?? []);

$relationType = $options['relationType'] ?? '';
$side = $options['side'] ?? '';

// Many-to-many is always an array on both sides
if ($relationType === Database::RELATION_MANY_TO_MANY) {
return true;
}

// One-to-many: array on parent side, single on child side
if ($relationType === Database::RELATION_ONE_TO_MANY && $side === Database::RELATION_SIDE_PARENT) {
return true;
}

// Many-to-one: array on child side, single on parent side
if ($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_CHILD) {
return true;
}

return false;
}

/**
* Get Description
*
Expand Down Expand Up @@ -165,7 +209,19 @@ private function validateOperatorForAttribute(
break;
case DatabaseOperator::TYPE_ARRAY_APPEND:
case DatabaseOperator::TYPE_ARRAY_PREPEND:
if (!$isArray) {
// For relationships, check if it's a "many" side
if ($type === Database::VAR_RELATIONSHIP) {
if (!$this->isRelationshipArray($attribute)) {
$this->message = "Cannot apply {$method} operator to single-value relationship '{$operator->getAttribute()}'";
return false;
}
foreach ($values as $item) {
if (!$this->isValidRelationshipValue($item)) {
$this->message = "Cannot apply {$method} operator: relationship values must be document IDs (strings) or Document objects";
return false;
}
}
} elseif (!$isArray) {
$this->message = "Cannot apply {$method} operator to non-array field '{$operator->getAttribute()}'";
return false;
}
Expand All @@ -182,14 +238,24 @@ private function validateOperatorForAttribute(

break;
case DatabaseOperator::TYPE_ARRAY_UNIQUE:
if (!$isArray) {
if ($type === Database::VAR_RELATIONSHIP) {
if (!$this->isRelationshipArray($attribute)) {
$this->message = "Cannot apply {$method} operator to single-value relationship '{$operator->getAttribute()}'";
return false;
}
} elseif (!$isArray) {
$this->message = "Cannot apply {$method} operator to non-array field '{$operator->getAttribute()}'";
return false;
}

break;
case DatabaseOperator::TYPE_ARRAY_INSERT:
if (!$isArray) {
if ($type === Database::VAR_RELATIONSHIP) {
if (!$this->isRelationshipArray($attribute)) {
$this->message = "Cannot apply {$method} operator to single-value relationship '{$operator->getAttribute()}'";
return false;
}
} elseif (!$isArray) {
$this->message = "Cannot apply {$method} operator to non-array field '{$operator->getAttribute()}'";
return false;
}
Expand All @@ -206,6 +272,14 @@ private function validateOperatorForAttribute(
}

$insertValue = $values[1];

if ($type === Database::VAR_RELATIONSHIP) {
if (!$this->isValidRelationshipValue($insertValue)) {
$this->message = "Cannot apply {$method} operator: relationship values must be document IDs (strings) or Document objects";
return false;
}
}

if ($type === Database::VAR_INTEGER && \is_numeric($insertValue)) {
if ($insertValue > Database::MAX_INT || $insertValue < Database::MIN_INT) {
$this->message = "Cannot apply {$method} operator: array items must be between " . Database::MIN_INT . " and " . Database::MAX_INT;
Expand All @@ -228,7 +302,19 @@ private function validateOperatorForAttribute(

break;
case DatabaseOperator::TYPE_ARRAY_REMOVE:
if (!$isArray) {
if ($type === Database::VAR_RELATIONSHIP) {
if (!$this->isRelationshipArray($attribute)) {
$this->message = "Cannot apply {$method} operator to single-value relationship '{$operator->getAttribute()}'";
return false;
}
$toValidate = \is_array($values[0]) ? $values[0] : $values;
foreach ($toValidate as $item) {
if (!$this->isValidRelationshipValue($item)) {
$this->message = "Cannot apply {$method} operator: relationship values must be document IDs (strings) or Document objects";
return false;
}
}
} elseif (!$isArray) {
$this->message = "Cannot apply {$method} operator to non-array field '{$operator->getAttribute()}'";
return false;
}
Expand All @@ -240,7 +326,12 @@ private function validateOperatorForAttribute(

break;
case DatabaseOperator::TYPE_ARRAY_INTERSECT:
if (!$isArray) {
if ($type === Database::VAR_RELATIONSHIP) {
if (!$this->isRelationshipArray($attribute)) {
$this->message = "Cannot apply {$method} operator to single-value relationship '{$operator->getAttribute()}'";
return false;
}
} elseif (!$isArray) {
$this->message = "Cannot use {$method} operator on non-array attribute '{$operator->getAttribute()}'";
return false;
}
Expand All @@ -250,16 +341,41 @@ private function validateOperatorForAttribute(
return false;
}

if ($type === Database::VAR_RELATIONSHIP) {
foreach ($values as $item) {
if (!$this->isValidRelationshipValue($item)) {
$this->message = "Cannot apply {$method} operator: relationship values must be document IDs (strings) or Document objects";
return false;
}
}
}

break;
case DatabaseOperator::TYPE_ARRAY_DIFF:
if (!$isArray) {
if ($type === Database::VAR_RELATIONSHIP) {
if (!$this->isRelationshipArray($attribute)) {
$this->message = "Cannot apply {$method} operator to single-value relationship '{$operator->getAttribute()}'";
return false;
}
foreach ($values as $item) {
if (!$this->isValidRelationshipValue($item)) {
$this->message = "Cannot apply {$method} operator: relationship values must be document IDs (strings) or Document objects";
return false;
}
}
} elseif (!$isArray) {
$this->message = "Cannot use {$method} operator on non-array attribute '{$operator->getAttribute()}'";
return false;
}

break;
case DatabaseOperator::TYPE_ARRAY_FILTER:
if (!$isArray) {
if ($type === Database::VAR_RELATIONSHIP) {
if (!$this->isRelationshipArray($attribute)) {
$this->message = "Cannot apply {$method} operator to single-value relationship '{$operator->getAttribute()}'";
return false;
}
} elseif (!$isArray) {
$this->message = "Cannot apply {$method} operator to non-array field '{$operator->getAttribute()}'";
return false;
}
Expand Down
Loading