994 lines
27 KiB
PHP
994 lines
27 KiB
PHP
<?php
|
|
|
|
namespace BaoPham\DynamoDb;
|
|
|
|
use BaoPham\DynamoDb\Concerns\HasParsers;
|
|
use BaoPham\DynamoDb\ConditionAnalyzer\Analyzer;
|
|
use BaoPham\DynamoDb\Facades\DynamoDb;
|
|
use BaoPham\DynamoDb\H;
|
|
use Closure;
|
|
use Illuminate\Contracts\Support\Arrayable;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Database\Eloquent\Scope;
|
|
use Illuminate\Support\Arr;
|
|
|
|
class DynamoDbQueryBuilder
|
|
{
|
|
use HasParsers;
|
|
|
|
const MAX_LIMIT = -1;
|
|
const DEFAULT_TO_ITERATOR = true;
|
|
|
|
/**
|
|
* The maximum number of records to return.
|
|
*
|
|
* @var int
|
|
*/
|
|
public $limit;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
public $wheres = [];
|
|
|
|
/**
|
|
* @var DynamoDbModel
|
|
*/
|
|
protected $model;
|
|
|
|
/**
|
|
* @var \Aws\DynamoDb\DynamoDbClient
|
|
*/
|
|
protected $client;
|
|
|
|
/**
|
|
* @var Closure
|
|
*/
|
|
protected $decorator;
|
|
|
|
/**
|
|
* Applied global scopes.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $scopes = [];
|
|
|
|
/**
|
|
* Removed global scopes.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $removedScopes = [];
|
|
|
|
/**
|
|
* When not using the iterator, you can store the lastEvaluatedKey to
|
|
* paginate through the results. The getAll method will take this into account
|
|
* when used with $use_iterator = false.
|
|
*
|
|
* @var mixed
|
|
*/
|
|
protected $lastEvaluatedKey;
|
|
|
|
/**
|
|
* Specified index name for the query.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $index;
|
|
|
|
public function __construct(DynamoDbModel $model)
|
|
{
|
|
$this->model = $model;
|
|
$this->client = $model->getClient();
|
|
$this->setupExpressions();
|
|
}
|
|
|
|
/**
|
|
* Alias to set the "limit" value of the query.
|
|
*
|
|
* @param int $value
|
|
* @return DynamoDbQueryBuilder
|
|
*/
|
|
public function take($value)
|
|
{
|
|
return $this->limit($value);
|
|
}
|
|
|
|
/**
|
|
* Set the "limit" value of the query.
|
|
*
|
|
* @param int $value
|
|
* @return $this
|
|
*/
|
|
public function limit($value)
|
|
{
|
|
$this->limit = $value;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Alias to set the "offset" value of the query.
|
|
*
|
|
* @param int $value
|
|
* @throws NotSupportedException
|
|
*/
|
|
public function skip($value)
|
|
{
|
|
return $this->offset($value);
|
|
}
|
|
|
|
/**
|
|
* Set the "offset" value of the query.
|
|
*
|
|
* @param int $value
|
|
* @throws NotSupportedException
|
|
*/
|
|
public function offset($value)
|
|
{
|
|
throw new NotSupportedException('Skip/Offset is not supported. Consider using after() instead');
|
|
}
|
|
|
|
/**
|
|
* Determine the starting point (exclusively) of the query.
|
|
* Unfortunately, offset of how many records to skip does not make sense for DynamoDb.
|
|
* Instead, provide the last result of the previous query as the starting point for the next query.
|
|
*
|
|
* @param DynamoDbModel|null $after
|
|
* Examples:
|
|
*
|
|
* For query such as
|
|
* $query = $model->where('count', 10)->limit(2);
|
|
* $last = $query->all()->last();
|
|
* Take the last item of this query result as the next "offset":
|
|
* $nextPage = $query->after($last)->limit(2)->all();
|
|
*
|
|
* Alternatively, pass in nothing to reset the starting point.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function after(DynamoDbModel $after = null)
|
|
{
|
|
if (empty($after)) {
|
|
$this->lastEvaluatedKey = null;
|
|
|
|
return $this;
|
|
}
|
|
|
|
$afterKey = $after->getKeys();
|
|
|
|
$analyzer = $this->getConditionAnalyzer();
|
|
|
|
if ($index = $analyzer->index()) {
|
|
foreach ($index->columns() as $column) {
|
|
$afterKey[$column] = $after->getAttribute($column);
|
|
}
|
|
}
|
|
|
|
$this->lastEvaluatedKey = DynamoDb::marshalItem($afterKey);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Similar to after(), but instead of using the model instance, the model's keys are used.
|
|
* Use $collection->lastKey() or $model->getKeys() to retrieve the value.
|
|
*
|
|
* @param Array $key
|
|
* Examples:
|
|
*
|
|
* For query such as
|
|
* $query = $model->where('count', 10)->limit(2);
|
|
* $items = $query->all();
|
|
* Take the last item of this query result as the next "offset":
|
|
* $nextPage = $query->afterKey($items->lastKey())->limit(2)->all();
|
|
*
|
|
* Alternatively, pass in nothing to reset the starting point.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function afterKey($key = null)
|
|
{
|
|
$this->lastEvaluatedKey = empty($key) ? null : DynamoDb::marshalItem($key);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the index name manually
|
|
*
|
|
* @param string $index The index name
|
|
* @return $this
|
|
*/
|
|
public function withIndex($index)
|
|
{
|
|
$this->index = $index;
|
|
return $this;
|
|
}
|
|
|
|
public function where($column, $operator = null, $value = null, $boolean = 'and')
|
|
{
|
|
// If the column is an array, we will assume it is an array of key-value pairs
|
|
// and can add them each as a where clause. We will maintain the boolean we
|
|
// received when the method was called and pass it into the nested where.
|
|
if (is_array($column)) {
|
|
foreach ($column as $key => $value) {
|
|
$this->where($key, '=', $value, $boolean);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Here we will make some assumptions about the operator. If only 2 values are
|
|
// passed to the method, we will assume that the operator is an equals sign
|
|
// and keep going. Otherwise, we'll require the operator to be passed in.
|
|
if (func_num_args() == 2) {
|
|
list($value, $operator) = [$operator, '='];
|
|
}
|
|
|
|
// If the columns is actually a Closure instance, we will assume the developer
|
|
// wants to begin a nested where statement which is wrapped in parenthesis.
|
|
// We'll add that Closure to the query then return back out immediately.
|
|
if ($column instanceof Closure) {
|
|
return $this->whereNested($column, $boolean);
|
|
}
|
|
|
|
// If the given operator is not found in the list of valid operators we will
|
|
// assume that the developer is just short-cutting the '=' operators and
|
|
// we will set the operators to '=' and set the values appropriately.
|
|
if (!ComparisonOperator::isValidOperator($operator)) {
|
|
list($value, $operator) = [$operator, '='];
|
|
}
|
|
|
|
// If the value is a Closure, it means the developer is performing an entire
|
|
// sub-select within the query and we will need to compile the sub-select
|
|
// within the where clause to get the appropriate query record results.
|
|
if ($value instanceof Closure) {
|
|
throw new NotSupportedException('Closure in where clause is not supported');
|
|
}
|
|
|
|
$this->wheres[] = [
|
|
'column' => $column,
|
|
'type' => ComparisonOperator::getDynamoDbOperator($operator),
|
|
'value' => $value,
|
|
'boolean' => $boolean,
|
|
];
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add a nested where statement to the query.
|
|
*
|
|
* @param \Closure $callback
|
|
* @param string $boolean
|
|
* @return $this
|
|
*/
|
|
public function whereNested(Closure $callback, $boolean = 'and')
|
|
{
|
|
call_user_func($callback, $query = $this->forNestedWhere());
|
|
|
|
return $this->addNestedWhereQuery($query, $boolean);
|
|
}
|
|
|
|
/**
|
|
* Create a new query instance for nested where condition.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function forNestedWhere()
|
|
{
|
|
return $this->newQuery();
|
|
}
|
|
|
|
/**
|
|
* Add another query builder as a nested where to the query builder.
|
|
*
|
|
* @param DynamoDbQueryBuilder $query
|
|
* @param string $boolean
|
|
* @return $this
|
|
*/
|
|
public function addNestedWhereQuery($query, $boolean = 'and')
|
|
{
|
|
if (count($query->wheres)) {
|
|
$type = 'Nested';
|
|
$column = null;
|
|
$value = $query->wheres;
|
|
$this->wheres[] = compact('column', 'type', 'value', 'boolean');
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add an "or where" clause to the query.
|
|
*
|
|
* @param string $column
|
|
* @param string $operator
|
|
* @param mixed $value
|
|
* @return $this
|
|
*/
|
|
public function orWhere($column, $operator = null, $value = null)
|
|
{
|
|
return $this->where($column, $operator, $value, 'or');
|
|
}
|
|
|
|
/**
|
|
* Add a "where in" clause to the query.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $values
|
|
* @param string $boolean
|
|
* @param bool $not
|
|
* @return $this
|
|
* @throws NotSupportedException
|
|
*/
|
|
public function whereIn($column, $values, $boolean = 'and', $not = false)
|
|
{
|
|
if ($not) {
|
|
throw new NotSupportedException('"not in" is not a valid DynamoDB comparison operator');
|
|
}
|
|
|
|
// If the value is a query builder instance, not supported
|
|
if ($values instanceof static) {
|
|
throw new NotSupportedException('Value is a query builder instance');
|
|
}
|
|
|
|
// If the value of the where in clause is actually a Closure, not supported
|
|
if ($values instanceof Closure) {
|
|
throw new NotSupportedException('Value is a Closure');
|
|
}
|
|
|
|
// Next, if the value is Arrayable we need to cast it to its raw array form
|
|
if ($values instanceof Arrayable) {
|
|
$values = $values->toArray();
|
|
}
|
|
|
|
return $this->where($column, ComparisonOperator::IN, $values, $boolean);
|
|
}
|
|
|
|
/**
|
|
* Add an "or where in" clause to the query.
|
|
*
|
|
* @param string $column
|
|
* @param mixed $values
|
|
* @return $this
|
|
*/
|
|
public function orWhereIn($column, $values)
|
|
{
|
|
return $this->whereIn($column, $values, 'or');
|
|
}
|
|
|
|
/**
|
|
* Add a "where null" clause to the query.
|
|
*
|
|
* @param string $column
|
|
* @param string $boolean
|
|
* @param bool $not
|
|
* @return $this
|
|
*/
|
|
public function whereNull($column, $boolean = 'and', $not = false)
|
|
{
|
|
$type = $not ? ComparisonOperator::NOT_NULL : ComparisonOperator::NULL;
|
|
|
|
$this->wheres[] = compact('column', 'type', 'boolean');
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add an "or where null" clause to the query.
|
|
*
|
|
* @param string $column
|
|
* @return $this
|
|
*/
|
|
public function orWhereNull($column)
|
|
{
|
|
return $this->whereNull($column, 'or');
|
|
}
|
|
|
|
/**
|
|
* Add an "or where not null" clause to the query.
|
|
*
|
|
* @param string $column
|
|
* @return $this
|
|
*/
|
|
public function orWhereNotNull($column)
|
|
{
|
|
return $this->whereNotNull($column, 'or');
|
|
}
|
|
|
|
/**
|
|
* Add a "where not null" clause to the query.
|
|
*
|
|
* @param string $column
|
|
* @param string $boolean
|
|
* @return $this
|
|
*/
|
|
public function whereNotNull($column, $boolean = 'and')
|
|
{
|
|
return $this->whereNull($column, $boolean, true);
|
|
}
|
|
|
|
/**
|
|
* Get a new instance of the query builder.
|
|
*
|
|
* @return DynamoDbQueryBuilder
|
|
*/
|
|
public function newQuery()
|
|
{
|
|
return new static($this->getModel());
|
|
}
|
|
|
|
/**
|
|
* Implements the Query Chunk method
|
|
*
|
|
* @param int $chunkSize
|
|
* @param callable $callback
|
|
*/
|
|
public function chunk($chunkSize, callable $callback)
|
|
{
|
|
while (true) {
|
|
$results = $this->getAll([], $chunkSize, false);
|
|
|
|
if (!$results->isEmpty()) {
|
|
if (call_user_func($callback, $results) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (empty($this->lastEvaluatedKey)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $id
|
|
* @param array $columns
|
|
* @return DynamoDbModel|\Illuminate\Database\Eloquent\Collection|null
|
|
*/
|
|
public function find($id, array $columns = [])
|
|
{
|
|
if ($this->isMultipleIds($id)) {
|
|
return $this->findMany($id, $columns);
|
|
}
|
|
|
|
$this->resetExpressions();
|
|
|
|
$this->model->setId($id);
|
|
|
|
$query = DynamoDb::table($this->model->getTable())
|
|
->setKey(DynamoDb::marshalItem($this->model->getKeys()))
|
|
->setConsistentRead(true);
|
|
|
|
if (!empty($columns)) {
|
|
$query
|
|
->setProjectionExpression($this->projectionExpression->parse($columns))
|
|
->setExpressionAttributeNames($this->expressionAttributeNames->all());
|
|
}
|
|
|
|
$item = $query->prepare($this->client)->getItem();
|
|
|
|
$item = Arr::get($item->toArray(), 'Item');
|
|
|
|
if (empty($item)) {
|
|
return null;
|
|
}
|
|
|
|
$item = DynamoDb::unmarshalItem($item);
|
|
|
|
$model = $this->model->newInstance([], true);
|
|
|
|
$model->setRawAttributes($item, true);
|
|
|
|
return $model;
|
|
}
|
|
|
|
/**
|
|
* @param $ids
|
|
* @param array $columns
|
|
* @return \Illuminate\Database\Eloquent\Collection
|
|
*/
|
|
public function findMany($ids, array $columns = [])
|
|
{
|
|
$collection = $this->model->newCollection();
|
|
|
|
if (empty($ids)) {
|
|
return $collection;
|
|
}
|
|
|
|
$this->resetExpressions();
|
|
|
|
$table = $this->model->getTable();
|
|
|
|
$keys = collect($ids)->map(function ($id) {
|
|
if (! is_array($id)) {
|
|
$id = [$this->model->getKeyName() => $id];
|
|
}
|
|
|
|
return DynamoDb::marshalItem($id);
|
|
});
|
|
|
|
$subQuery = DynamoDb::newQuery()
|
|
->setKeys($keys->toArray())
|
|
->setProjectionExpression($this->projectionExpression->parse($columns))
|
|
->setExpressionAttributeNames($this->expressionAttributeNames->all())
|
|
->prepare($this->client)
|
|
->query;
|
|
|
|
$results = DynamoDb::newQuery()
|
|
->setRequestItems([$table => $subQuery])
|
|
->prepare($this->client)
|
|
->batchGetItem();
|
|
|
|
foreach ($results['Responses'][$table] as $item) {
|
|
$item = DynamoDb::unmarshalItem($item);
|
|
$model = $this->model->newInstance([], true);
|
|
$model->setRawAttributes($item, true);
|
|
$collection->add($model);
|
|
}
|
|
|
|
return $collection;
|
|
}
|
|
|
|
public function findOrFail($id, $columns = [])
|
|
{
|
|
$result = $this->find($id, $columns);
|
|
|
|
if ($this->isMultipleIds($id)) {
|
|
if (count($result) == count(array_unique($id))) {
|
|
return $result;
|
|
}
|
|
} elseif (! is_null($result)) {
|
|
return $result;
|
|
}
|
|
|
|
throw (new ModelNotFoundException)->setModel(
|
|
get_class($this->model),
|
|
$id
|
|
);
|
|
}
|
|
|
|
public function first($columns = [])
|
|
{
|
|
$items = $this->getAll($columns, 1);
|
|
|
|
return $items->first();
|
|
}
|
|
|
|
public function firstOrFail($columns = [])
|
|
{
|
|
if (! is_null($model = $this->first($columns))) {
|
|
return $model;
|
|
}
|
|
|
|
throw (new ModelNotFoundException)->setModel(get_class($this->model));
|
|
}
|
|
|
|
/**
|
|
* Remove attributes from an existing item
|
|
*
|
|
* @param array ...$attributes
|
|
* @return bool
|
|
* @throws InvalidQuery
|
|
*/
|
|
public function removeAttribute(...$attributes)
|
|
{
|
|
$keySet = !empty(array_filter($this->model->getKeys()));
|
|
|
|
if (!$keySet) {
|
|
$analyzer = $this->getConditionAnalyzer();
|
|
|
|
if (!$analyzer->isExactSearch()) {
|
|
throw new InvalidQuery('Need to provide the key in your query');
|
|
}
|
|
|
|
$id = $analyzer->identifierConditionValues();
|
|
$this->model->setId($id);
|
|
}
|
|
|
|
$key = DynamoDb::marshalItem($this->model->getKeys());
|
|
|
|
$this->resetExpressions();
|
|
|
|
/** @var \Aws\Result $result */
|
|
$result = DynamoDb::table($this->model->getTable())
|
|
->setKey($key)
|
|
->setUpdateExpression($this->updateExpression->remove($attributes))
|
|
->setExpressionAttributeNames($this->expressionAttributeNames->all())
|
|
->setReturnValues('ALL_NEW')
|
|
->prepare($this->client)
|
|
->updateItem();
|
|
|
|
$success = Arr::get($result, '@metadata.statusCode') === 200;
|
|
|
|
if ($success) {
|
|
$this->model->setRawAttributes(DynamoDb::unmarshalItem($result->get('Attributes')));
|
|
$this->model->syncOriginal();
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
public function delete()
|
|
{
|
|
$result = DynamoDb::table($this->model->getTable())
|
|
->setKey(DynamoDb::marshalItem($this->model->getKeys()))
|
|
->prepare($this->client)
|
|
->deleteItem();
|
|
|
|
return Arr::get($result->toArray(), '@metadata.statusCode') === 200;
|
|
}
|
|
|
|
public function deleteAsync()
|
|
{
|
|
$promise = DynamoDb::table($this->model->getTable())
|
|
->setKey(DynamoDb::marshalItem($this->model->getKeys()))
|
|
->prepare($this->client)
|
|
->deleteItemAsync();
|
|
|
|
return $promise;
|
|
}
|
|
|
|
public function save()
|
|
{
|
|
$result = DynamoDb::table($this->model->getTable())
|
|
->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
|
|
->prepare($this->client)
|
|
->putItem();
|
|
|
|
return Arr::get($result, '@metadata.statusCode') === 200;
|
|
}
|
|
|
|
public function saveAsync()
|
|
{
|
|
$promise = DynamoDb::table($this->model->getTable())
|
|
->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
|
|
->prepare($this->client)
|
|
->putItemAsync();
|
|
|
|
return $promise;
|
|
}
|
|
|
|
public function get($columns = [])
|
|
{
|
|
return $this->all($columns);
|
|
}
|
|
|
|
public function all($columns = [])
|
|
{
|
|
$limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
|
|
return $this->getAll($columns, $limit, !isset($this->limit));
|
|
}
|
|
|
|
public function count()
|
|
{
|
|
$limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
|
|
$raw = $this->toDynamoDbQuery(['count(*)'], $limit);
|
|
|
|
if ($raw->op === 'Scan') {
|
|
$res = $this->client->scan($raw->query);
|
|
} else {
|
|
$res = $this->client->query($raw->query);
|
|
}
|
|
|
|
return $res['Count'];
|
|
}
|
|
|
|
public function decorate(Closure $closure)
|
|
{
|
|
$this->decorator = $closure;
|
|
return $this;
|
|
}
|
|
|
|
protected function getAll(
|
|
$columns = [],
|
|
$limit = DynamoDbQueryBuilder::MAX_LIMIT,
|
|
$useIterator = DynamoDbQueryBuilder::DEFAULT_TO_ITERATOR
|
|
) {
|
|
$analyzer = $this->getConditionAnalyzer();
|
|
|
|
if ($analyzer->isExactSearch()) {
|
|
$item = $this->find($analyzer->identifierConditionValues(), $columns);
|
|
|
|
return $this->getModel()->newCollection([$item]);
|
|
}
|
|
|
|
$raw = $this->toDynamoDbQuery($columns, $limit);
|
|
|
|
if ($useIterator) {
|
|
$iterator = $this->client->getIterator($raw->op, $raw->query);
|
|
|
|
if (isset($raw->query['Limit'])) {
|
|
$iterator = new \LimitIterator($iterator, 0, $raw->query['Limit']);
|
|
}
|
|
} else {
|
|
if ($raw->op === 'Scan') {
|
|
$res = $this->client->scan($raw->query);
|
|
} else {
|
|
$res = $this->client->query($raw->query);
|
|
}
|
|
|
|
$this->lastEvaluatedKey = Arr::get($res, 'LastEvaluatedKey');
|
|
$iterator = $res['Items'];
|
|
}
|
|
|
|
$results = [];
|
|
|
|
foreach ($iterator as $item) {
|
|
$item = DynamoDb::unmarshalItem($item);
|
|
$model = $this->model->newInstance([], true);
|
|
$model->setRawAttributes($item, true);
|
|
$results[] = $model;
|
|
}
|
|
|
|
return $this->getModel()->newCollection($results, $analyzer->index());
|
|
}
|
|
|
|
/**
|
|
* Return the raw DynamoDb query
|
|
*
|
|
* @param array $columns
|
|
* @param int $limit
|
|
* @return RawDynamoDbQuery
|
|
*/
|
|
public function toDynamoDbQuery(
|
|
$columns = [],
|
|
$limit = DynamoDbQueryBuilder::MAX_LIMIT
|
|
) {
|
|
$this->applyScopes();
|
|
|
|
$this->resetExpressions();
|
|
|
|
$op = 'Scan';
|
|
$queryBuilder = DynamoDb::table($this->model->getTable());
|
|
|
|
if (! empty($this->wheres)) {
|
|
$analyzer = $this->getConditionAnalyzer();
|
|
|
|
if ($keyConditions = $analyzer->keyConditions()) {
|
|
$op = 'Query';
|
|
$queryBuilder->setKeyConditionExpression($this->keyConditionExpression->parse($keyConditions));
|
|
}
|
|
|
|
if ($filterConditions = $analyzer->filterConditions()) {
|
|
$queryBuilder->setFilterExpression($this->filterExpression->parse($filterConditions));
|
|
}
|
|
|
|
if ($index = $analyzer->index()) {
|
|
$queryBuilder->setIndexName($index->name);
|
|
}
|
|
}
|
|
|
|
if ($this->index) {
|
|
// If user specifies the index manually, respect that
|
|
$queryBuilder->setIndexName($this->index);
|
|
}
|
|
|
|
if ($limit !== static::MAX_LIMIT) {
|
|
$queryBuilder->setLimit($limit);
|
|
}
|
|
|
|
if (!empty($columns)) {
|
|
// Either we try to get the count or specific columns
|
|
if ($columns == ['count(*)']) {
|
|
$queryBuilder->setSelect('COUNT');
|
|
} else {
|
|
$queryBuilder->setProjectionExpression($this->projectionExpression->parse($columns));
|
|
}
|
|
}
|
|
|
|
if (!empty($this->lastEvaluatedKey)) {
|
|
$queryBuilder->setExclusiveStartKey($this->lastEvaluatedKey);
|
|
}
|
|
|
|
$queryBuilder
|
|
->setExpressionAttributeNames($this->expressionAttributeNames->all())
|
|
->setExpressionAttributeValues($this->expressionAttributeValues->all());
|
|
|
|
$raw = new RawDynamoDbQuery($op, $queryBuilder->prepare($this->client)->query);
|
|
|
|
if ($this->decorator) {
|
|
call_user_func($this->decorator, $raw);
|
|
}
|
|
|
|
return $raw;
|
|
}
|
|
|
|
/**
|
|
* @return Analyzer
|
|
*/
|
|
protected function getConditionAnalyzer()
|
|
{
|
|
return with(new Analyzer)
|
|
->on($this->model)
|
|
->withIndex($this->index)
|
|
->analyze($this->wheres);
|
|
}
|
|
|
|
protected function isMultipleIds($id)
|
|
{
|
|
$keys = collect($this->model->getKeyNames());
|
|
|
|
// could be ['id' => 'foo'], ['id1' => 'foo', 'id2' => 'bar']
|
|
$single = $keys->first(function ($name) use ($id) {
|
|
return !isset($id[$name]);
|
|
}) === null;
|
|
|
|
if ($single) {
|
|
return false;
|
|
}
|
|
|
|
// could be ['foo', 'bar'], [['id1' => 'foo', 'id2' => 'bar'], ...]
|
|
return $this->model->hasCompositeKey() ? is_array(H::array_first($id)) : is_array($id);
|
|
}
|
|
|
|
/**
|
|
* @return DynamoDbModel
|
|
*/
|
|
public function getModel()
|
|
{
|
|
return $this->model;
|
|
}
|
|
|
|
/**
|
|
* @return \Aws\DynamoDb\DynamoDbClient
|
|
*/
|
|
public function getClient()
|
|
{
|
|
return $this->client;
|
|
}
|
|
|
|
/**
|
|
* Register a new global scope.
|
|
*
|
|
* @param string $identifier
|
|
* @param \Illuminate\Database\Eloquent\Scope|\Closure $scope
|
|
* @return $this
|
|
*/
|
|
public function withGlobalScope($identifier, $scope)
|
|
{
|
|
$this->scopes[$identifier] = $scope;
|
|
|
|
if (method_exists($scope, 'extend')) {
|
|
$scope->extend($this);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Remove a registered global scope.
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Scope|string $scope
|
|
* @return $this
|
|
*/
|
|
public function withoutGlobalScope($scope)
|
|
{
|
|
if (! is_string($scope)) {
|
|
$scope = get_class($scope);
|
|
}
|
|
|
|
unset($this->scopes[$scope]);
|
|
|
|
$this->removedScopes[] = $scope;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Remove all or passed registered global scopes.
|
|
*
|
|
* @param array|null $scopes
|
|
* @return $this
|
|
*/
|
|
public function withoutGlobalScopes(array $scopes = null)
|
|
{
|
|
if (is_array($scopes)) {
|
|
foreach ($scopes as $scope) {
|
|
$this->withoutGlobalScope($scope);
|
|
}
|
|
} else {
|
|
$this->scopes = [];
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get an array of global scopes that were removed from the query.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function removedScopes()
|
|
{
|
|
return $this->removedScopes;
|
|
}
|
|
|
|
/**
|
|
* Apply the scopes to the Eloquent builder instance and return it.
|
|
*
|
|
* @return DynamoDbQueryBuilder
|
|
*/
|
|
public function applyScopes()
|
|
{
|
|
if (! $this->scopes) {
|
|
return $this;
|
|
}
|
|
|
|
$builder = $this;
|
|
|
|
foreach ($builder->scopes as $identifier => $scope) {
|
|
if (! isset($builder->scopes[$identifier])) {
|
|
continue;
|
|
}
|
|
|
|
$builder->callScope(function (DynamoDbQueryBuilder $builder) use ($scope) {
|
|
// If the scope is a Closure we will just go ahead and call the scope with the
|
|
// builder instance. The "callScope" method will properly group the clauses
|
|
// that are added to this query so "where" clauses maintain proper logic.
|
|
if ($scope instanceof Closure) {
|
|
$scope($builder);
|
|
}
|
|
|
|
// If the scope is a scope object, we will call the apply method on this scope
|
|
// passing in the builder and the model instance. After we run all of these
|
|
// scopes we will return back the builder instance to the outside caller.
|
|
if ($scope instanceof Scope) {
|
|
throw new NotSupportedException('Scope object is not yet supported');
|
|
}
|
|
});
|
|
|
|
$builder->withoutGlobalScope($identifier);
|
|
}
|
|
|
|
return $builder;
|
|
}
|
|
|
|
/**
|
|
* Apply the given scope on the current builder instance.
|
|
*
|
|
* @param callable $scope
|
|
* @param array $parameters
|
|
* @return mixed
|
|
*/
|
|
protected function callScope(callable $scope, $parameters = [])
|
|
{
|
|
array_unshift($parameters, $this);
|
|
|
|
// $query = $this->getQuery();
|
|
|
|
// // We will keep track of how many wheres are on the query before running the
|
|
// // scope so that we can properly group the added scope constraints in the
|
|
// // query as their own isolated nested where statement and avoid issues.
|
|
// $originalWhereCount = is_null($query->wheres)
|
|
// ? 0 : count($query->wheres);
|
|
|
|
$result = $scope(...array_values($parameters)) ?: $this;
|
|
|
|
// if (count((array) $query->wheres) > $originalWhereCount) {
|
|
// $this->addNewWheresWithinGroup($query, $originalWhereCount);
|
|
// }
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Dynamically handle calls into the query instance.
|
|
*
|
|
* @param string $method
|
|
* @param array $parameters
|
|
* @return mixed
|
|
*/
|
|
public function __call($method, $parameters)
|
|
{
|
|
if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
|
|
return $this->callScope([$this->model, $scope], $parameters);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
}
|