gdbots/ncr-php

View on GitHub
src/Repository/DynamoDb/AbstractIndex.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php
declare(strict_types=1);

namespace Gdbots\Ncr\Repository\DynamoDb;

use Gdbots\Ncr\Enum\IndexQueryFilterOperator;
use Gdbots\Ncr\Exception\IndexQueryNotSupported;
use Gdbots\Ncr\IndexQuery;
use Gdbots\Pbj\Message;

abstract class AbstractIndex implements GlobalSecondaryIndex
{
    public function getName(): string
    {
        return "{$this->getAlias()}_index";
    }

    public function getRangeKeyName(): ?string
    {
        return null;
    }

    public function getFilterableAttributes(): array
    {
        return [];
    }

    public function getProjection(): array
    {
        return [];
    }

    public function beforePutItem(array &$item, Message $node): void
    {
    }

    final public function createQuery(IndexQuery $query): array
    {
        $filterables = $this->getFilterableAttributes();
        $params = [
            'IndexName'                 => $this->getName(),
            'ScanIndexForward'          => $query->sortAsc(),
            //'Limit'                     => $query->getCount(),
            'ExpressionAttributeNames'  => [
                '#hash'     => $this->getHashKeyName(),
                '#node_ref' => NodeTable::HASH_KEY_NAME,
            ],
            'ExpressionAttributeValues' => [
                ':v_hash'  => ['S' => $query->getValue()],
                ':v_qname' => ['S' => $query->getQName()->toString() . ':'],
            ],
            // special key to deal with filters that must be
            // processed AFTER the query runs due to limitation of range key
            // only allowing for one expression.
            'unprocessed_filters'       => [],
        ];

        $keyConditionExpressions = ['#hash = :v_hash'];
        $filterExpressions = ['begins_with(#node_ref, :v_qname)'];
        $rangeKeyName = $this->getRangeKeyName();
        $usedRange = false;
        $i = 0;

        foreach ($query->getFilters() as $filter) {
            $i++;
            $fieldName = $filter->getField();

            if (!isset($filterables[$fieldName])) {
                throw new IndexQueryNotSupported(
                    sprintf(
                        'Field [%s] is not supported on index [%s].  Supported fields: %s',
                        $fieldName,
                        $this->getAlias(),
                        implode(', ', array_keys($filterables))
                    )
                );
            }

            $filterable = $filterables[$fieldName];

            $ean = "#n_{$filterable['AttributeName']}";
            $eav = ":v_{$filterable['AttributeName']}_{$i}";

            $params['ExpressionAttributeNames'][$ean] = $filterable['AttributeName'];
            $params['ExpressionAttributeValues'][$eav] = [
                $filterable['AttributeType'] => $this->marshalValue($filterable['AttributeType'], $filter->getValue()),
            ];

            $operator = $this->getDynamoDbOperator($filter->getOperator());

            if ($rangeKeyName === $fieldName) {
                if ($usedRange) {
                    $params['unprocessed_filters'][] = $filter;
                    unset($params['ExpressionAttributeValues'][$eav]);
                    continue;
                }

                $keyConditionExpressions[] = "$ean $operator $eav";
                $usedRange = true;
                continue;
            }

            $filterExpressions[] = "$ean $operator $eav";
        }

        if ($query->hasCursor()) {
            $params['ExclusiveStartKey'] = json_decode(base64_decode($query->getCursor()), true);
        }

        $params['KeyConditionExpression'] = implode(' AND ', $keyConditionExpressions);
        $params['FilterExpression'] = implode(' AND ', $filterExpressions);

        if (empty($params['FilterExpression'])) {
            unset($params['FilterExpression']);
        }

        if (empty($params['unprocessed_filters'])) {
            unset($params['unprocessed_filters']);
        }

        return $params;
    }

    private function marshalValue(string $attributeType, mixed $value): bool|string
    {
        return match ($attributeType) {
            'BOOL' => filter_var($value, FILTER_VALIDATE_BOOLEAN),
            default => (string)$value,
        };
    }

    private function getDynamoDbOperator(IndexQueryFilterOperator $operator): string
    {
        return match ($operator) {
            IndexQueryFilterOperator::NOT_EQUAL_TO => '<>',
            IndexQueryFilterOperator::GREATER_THAN => '>',
            IndexQueryFilterOperator::GREATER_THAN_OR_EQUAL_TO => '>=',
            IndexQueryFilterOperator::LESS_THAN => '<',
            IndexQueryFilterOperator::LESS_THAN_OR_EQUAL_TO => '<=',
            default => '=',
        };
    }
}