Passed
Pull Request — master (#179)
by
unknown
06:32
created

DynamoDbQueryBuilder::afterKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
c 0
b 0
f 0
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
namespace BaoPham\DynamoDb;
4
5
use BaoPham\DynamoDb\Concerns\HasParsers;
6
use BaoPham\DynamoDb\ConditionAnalyzer\Analyzer;
7
use BaoPham\DynamoDb\Facades\DynamoDb;
8
use BaoPham\DynamoDb\H;
9
use Closure;
10
use Illuminate\Contracts\Support\Arrayable;
11
use Illuminate\Database\Eloquent\ModelNotFoundException;
12
use Illuminate\Database\Eloquent\Scope;
13
14
class DynamoDbQueryBuilder
15
{
16
    use HasParsers;
17
18
    const MAX_LIMIT = -1;
19
    const DEFAULT_TO_ITERATOR = true;
20
21
    /**
22
     * The maximum number of records to return.
23
     *
24
     * @var int
25
     */
26
    public $limit;
27
28
    /**
29
     * @var array
30
     */
31
    public $wheres = [];
32
33
    /**
34
     * @var DynamoDbModel
35
     */
36
    protected $model;
37
38
    /**
39
     * @var \Aws\DynamoDb\DynamoDbClient
40
     */
41
    protected $client;
42
43
    /**
44
     * @var Closure
45
     */
46
    protected $decorator;
47
48
    /**
49
     * Applied global scopes.
50
     *
51
     * @var array
52
     */
53
    protected $scopes = [];
54
55
    /**
56
     * Removed global scopes.
57
     *
58
     * @var array
59
     */
60
    protected $removedScopes = [];
61
62
    /**
63
     * When not using the iterator, you can store the lastEvaluatedKey to
64
     * paginate through the results. The getAll method will take this into account
65
     * when used with $use_iterator = false.
66
     *
67
     * @var mixed
68
     */
69
    protected $lastEvaluatedKey;
70
71
    /**
72
     * Specified index name for the query.
73
     *
74
     * @var string
75
     */
76
    protected $index;
77
78 143
    public function __construct(DynamoDbModel $model)
79
    {
80 143
        $this->model = $model;
81 143
        $this->client = $model->getClient();
82 143
        $this->setupExpressions();
83 143
    }
84
85
    /**
86
     * Alias to set the "limit" value of the query.
87
     *
88
     * @param  int  $value
89
     * @return DynamoDbQueryBuilder
90
     */
91 6
    public function take($value)
92
    {
93 6
        return $this->limit($value);
94
    }
95
96
    /**
97
     * Set the "limit" value of the query.
98
     *
99
     * @param  int  $value
100
     * @return $this
101
     */
102 14
    public function limit($value)
103
    {
104 14
        $this->limit = $value;
105
106 14
        return $this;
107
    }
108
109
    /**
110
     * Alias to set the "offset" value of the query.
111
     *
112
     * @param  int $value
113
     * @throws NotSupportedException
114
     */
115
    public function skip($value)
116
    {
117
        return $this->offset($value);
118
    }
119
120
    /**
121
     * Set the "offset" value of the query.
122
     *
123
     * @param  int $value
124
     * @throws NotSupportedException
125
     */
126
    public function offset($value)
127
    {
128
        throw new NotSupportedException('Skip/Offset is not supported. Consider using after() instead');
129
    }
130
131
    /**
132
     * Determine the starting point (exclusively) of the query.
133
     * Unfortunately, offset of how many records to skip does not make sense for DynamoDb.
134
     * Instead, provide the last result of the previous query as the starting point for the next query.
135
     *
136
     * @param  DynamoDbModel|null  $after
137
     *   Examples:
138
     *
139
     *   For query such as
140
     *       $query = $model->where('count', 10)->limit(2);
141
     *       $last = $query->all()->last();
142
     *   Take the last item of this query result as the next "offset":
143
     *       $nextPage = $query->after($last)->limit(2)->all();
144
     *
145
     *   Alternatively, pass in nothing to reset the starting point.
146
     *
147
     * @return $this
148
     */
149 4
    public function after(DynamoDbModel $after = null)
150
    {
151 4
        if (empty($after)) {
152 4
            $this->lastEvaluatedKey = null;
153
154 4
            return $this;
155
        }
156
157 4
        $afterKey = $after->getKeys();
158
159 4
        $analyzer = $this->getConditionAnalyzer();
160
161 4
        if ($index = $analyzer->index()) {
162 1
            foreach ($index->columns() as $column) {
163 1
                $afterKey[$column] = $after->getAttribute($column);
164
            }
165
        }
166
167 4
        $this->lastEvaluatedKey = DynamoDb::marshalItem($afterKey);
168
169 4
        return $this;
170
    }
171
172
    /**
173
     * Similar to after(), but instead of using the model instance, the model's keys are used.
174
     * Use $collection->lastKey() or $model->getKeys() to retrieve the value.
175
     *
176
     * @param  Array  $key
177
     *   Examples:
178
     *
179
     *   For query such as
180
     *       $query = $model->where('count', 10)->limit(2);
181
     *       $items = $query->all();
182
     *   Take the last item of this query result as the next "offset":
183
     *       $nextPage = $query->afterKey($items->lastKey())->limit(2)->all();
184
     *
185
     *   Alternatively, pass in nothing to reset the starting point.
186
     *
187
     * @return $this
188
     */
189 4
    public function afterKey($key = null)
190
    {
191 4
        $this->lastEvaluatedKey = empty($key) ? null : DynamoDb::marshalItem($key);
192 4
        return $this;
193
    }
194
195
    /**
196
     * Set the index name manually
197
     *
198
     * @param string $index The index name
199
     * @return $this
200
     */
201 1
    public function withIndex($index)
202
    {
203 1
        $this->index = $index;
204 1
        return $this;
205
    }
206
207 88
    public function where($column, $operator = null, $value = null, $boolean = 'and')
208
    {
209
        // If the column is an array, we will assume it is an array of key-value pairs
210
        // and can add them each as a where clause. We will maintain the boolean we
211
        // received when the method was called and pass it into the nested where.
212 88
        if (is_array($column)) {
213 14
            foreach ($column as $key => $value) {
214 14
                $this->where($key, '=', $value, $boolean);
215
            }
216
217 14
            return $this;
218
        }
219
220
        // Here we will make some assumptions about the operator. If only 2 values are
221
        // passed to the method, we will assume that the operator is an equals sign
222
        // and keep going. Otherwise, we'll require the operator to be passed in.
223 88
        if (func_num_args() == 2) {
224 45
            list($value, $operator) = [$operator, '='];
225
        }
226
227
        // If the columns is actually a Closure instance, we will assume the developer
228
        // wants to begin a nested where statement which is wrapped in parenthesis.
229
        // We'll add that Closure to the query then return back out immediately.
230 88
        if ($column instanceof Closure) {
231 2
            return $this->whereNested($column, $boolean);
232
        }
233
234
        // If the given operator is not found in the list of valid operators we will
235
        // assume that the developer is just short-cutting the '=' operators and
236
        // we will set the operators to '=' and set the values appropriately.
237 88
        if (!ComparisonOperator::isValidOperator($operator)) {
238 4
            list($value, $operator) = [$operator, '='];
239
        }
240
241
        // If the value is a Closure, it means the developer is performing an entire
242
        // sub-select within the query and we will need to compile the sub-select
243
        // within the where clause to get the appropriate query record results.
244 88
        if ($value instanceof Closure) {
245
            throw new NotSupportedException('Closure in where clause is not supported');
246
        }
247
248 88
        $this->wheres[] = [
249 88
            'column' => $column,
250 88
            'type' => ComparisonOperator::getDynamoDbOperator($operator),
251 88
            'value' => $value,
252 88
            'boolean' => $boolean,
253
        ];
254
255 88
        return $this;
256
    }
257
258
    /**
259
     * Add a nested where statement to the query.
260
     *
261
     * @param  \Closure $callback
262
     * @param  string   $boolean
263
     * @return $this
264
     */
265 2
    public function whereNested(Closure $callback, $boolean = 'and')
266
    {
267 2
        call_user_func($callback, $query = $this->forNestedWhere());
268
269 2
        return $this->addNestedWhereQuery($query, $boolean);
270
    }
271
272
    /**
273
     * Create a new query instance for nested where condition.
274
     *
275
     * @return $this
276
     */
277 2
    public function forNestedWhere()
278
    {
279 2
        return $this->newQuery();
280
    }
281
282
    /**
283
     * Add another query builder as a nested where to the query builder.
284
     *
285
     * @param  DynamoDbQueryBuilder $query
286
     * @param  string  $boolean
287
     * @return $this
288
     */
289 2
    public function addNestedWhereQuery($query, $boolean = 'and')
290
    {
291 2
        if (count($query->wheres)) {
292 2
            $type = 'Nested';
293 2
            $column = null;
294 2
            $value = $query->wheres;
295 2
            $this->wheres[] = compact('column', 'type', 'value', 'boolean');
296
        }
297
298 2
        return $this;
299
    }
300
301
    /**
302
     * Add an "or where" clause to the query.
303
     *
304
     * @param  string  $column
305
     * @param  string  $operator
306
     * @param  mixed   $value
307
     * @return $this
308
     */
309 22
    public function orWhere($column, $operator = null, $value = null)
310
    {
311 22
        return $this->where($column, $operator, $value, 'or');
312
    }
313
314
    /**
315
     * Add a "where in" clause to the query.
316
     *
317
     * @param  string  $column
318
     * @param  mixed   $values
319
     * @param  string  $boolean
320
     * @param  bool    $not
321
     * @return $this
322
     * @throws NotSupportedException
323
     */
324 2
    public function whereIn($column, $values, $boolean = 'and', $not = false)
325
    {
326 2
        if ($not) {
327
            throw new NotSupportedException('"not in" is not a valid DynamoDB comparison operator');
328
        }
329
330
        // If the value is a query builder instance, not supported
331 2
        if ($values instanceof static) {
332
            throw new NotSupportedException('Value is a query builder instance');
333
        }
334
335
        // If the value of the where in clause is actually a Closure, not supported
336 2
        if ($values instanceof Closure) {
337
            throw new NotSupportedException('Value is a Closure');
338
        }
339
340
        // Next, if the value is Arrayable we need to cast it to its raw array form
341 2
        if ($values instanceof Arrayable) {
342
            $values = $values->toArray();
343
        }
344
345 2
        return $this->where($column, ComparisonOperator::IN, $values, $boolean);
346
    }
347
348
    /**
349
     * Add an "or where in" clause to the query.
350
     *
351
     * @param  string  $column
352
     * @param  mixed   $values
353
     * @return $this
354
     */
355 2
    public function orWhereIn($column, $values)
356
    {
357 2
        return $this->whereIn($column, $values, 'or');
358
    }
359
360
    /**
361
     * Add a "where null" clause to the query.
362
     *
363
     * @param  string  $column
364
     * @param  string  $boolean
365
     * @param  bool    $not
366
     * @return $this
367
     */
368 4
    public function whereNull($column, $boolean = 'and', $not = false)
369
    {
370 4
        $type = $not ? ComparisonOperator::NOT_NULL : ComparisonOperator::NULL;
371
372 4
        $this->wheres[] = compact('column', 'type', 'boolean');
373
374 4
        return $this;
375
    }
376
377
    /**
378
     * Add an "or where null" clause to the query.
379
     *
380
     * @param  string  $column
381
     * @return $this
382
     */
383 2
    public function orWhereNull($column)
384
    {
385 2
        return $this->whereNull($column, 'or');
386
    }
387
388
    /**
389
     * Add an "or where not null" clause to the query.
390
     *
391
     * @param  string  $column
392
     * @return $this
393
     */
394 2
    public function orWhereNotNull($column)
395
    {
396 2
        return $this->whereNotNull($column, 'or');
397
    }
398
399
    /**
400
     * Add a "where not null" clause to the query.
401
     *
402
     * @param  string  $column
403
     * @param  string  $boolean
404
     * @return $this
405
     */
406 2
    public function whereNotNull($column, $boolean = 'and')
407
    {
408 2
        return $this->whereNull($column, $boolean, true);
409
    }
410
411
    /**
412
     * Get a new instance of the query builder.
413
     *
414
     * @return DynamoDbQueryBuilder
415
     */
416 2
    public function newQuery()
417
    {
418 2
        return new static($this->getModel());
419
    }
420
421
    /**
422
     * Implements the Query Chunk method
423
     *
424
     * @param int $chunkSize
425
     * @param callable $callback
426
     */
427 9
    public function chunk($chunkSize, callable $callback)
428
    {
429 9
        while (true) {
430 9
            $results = $this->getAll([], $chunkSize, false);
431
432 9
            if (!$results->isEmpty()) {
433 9
                if (call_user_func($callback, $results) === false) {
434 2
                    return false;
435
                }
436
            }
437
438 7
            if (empty($this->lastEvaluatedKey)) {
439 7
                break;
440
            }
441
        }
442
443 7
        return true;
444
    }
445
446
    /**
447
     * @param $id
448
     * @param array $columns
449
     * @return DynamoDbModel|\Illuminate\Database\Eloquent\Collection|null
450
     */
451 52
    public function find($id, array $columns = [])
452
    {
453 52
        if ($this->isMultipleIds($id)) {
454 4
            return $this->findMany($id, $columns);
455
        }
456
457 48
        $this->resetExpressions();
458
459 48
        $this->model->setId($id);
460
461 48
        $query = DynamoDb::table($this->model->getTable())
462 48
            ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
463 48
            ->setConsistentRead(true);
464
465 48
        if (!empty($columns)) {
466
            $query
467 3
                ->setProjectionExpression($this->projectionExpression->parse($columns))
468 3
                ->setExpressionAttributeNames($this->expressionAttributeNames->all());
469
        }
470
471 48
        $item = $query->prepare($this->client)->getItem();
472
473 48
        $item = array_get($item->toArray(), 'Item');
474
475 48
        if (empty($item)) {
476 10
            return null;
477
        }
478
479 38
        $item = DynamoDb::unmarshalItem($item);
480
481 38
        $model = $this->model->newInstance([], true);
482
483 38
        $model->setRawAttributes($item, true);
484
485 38
        return $model;
486
    }
487
488
    /**
489
     * @param $ids
490
     * @param array $columns
491
     * @return \Illuminate\Database\Eloquent\Collection
492
     */
493 4
    public function findMany($ids, array $columns = [])
494
    {
495 4
        $collection = $this->model->newCollection();
496
497 4
        if (empty($ids)) {
498
            return $collection;
499
        }
500
501 4
        $this->resetExpressions();
502
503 4
        $table = $this->model->getTable();
504
505
        $keys = collect($ids)->map(function ($id) {
506 4
            if (! is_array($id)) {
507 2
                $id = [$this->model->getKeyName() => $id];
508
            }
509
510 4
            return DynamoDb::marshalItem($id);
511 4
        });
512
513 4
        $subQuery = DynamoDb::newQuery()
514 4
            ->setKeys($keys->toArray())
515 4
            ->setProjectionExpression($this->projectionExpression->parse($columns))
516 4
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
517 4
            ->prepare($this->client)
518 4
            ->query;
519
520 4
        $results = DynamoDb::newQuery()
521 4
            ->setRequestItems([$table => $subQuery])
522 4
            ->prepare($this->client)
523 4
            ->batchGetItem();
524
525 4
        foreach ($results['Responses'][$table] as $item) {
526 4
            $item = DynamoDb::unmarshalItem($item);
527 4
            $model = $this->model->newInstance([], true);
528 4
            $model->setRawAttributes($item, true);
529 4
            $collection->add($model);
530
        }
531
532 4
        return $collection;
533
    }
534
535 8
    public function firstOrNew(array $attributes, array $values = [])
536
    {
537 8
        if (! is_null($instance = $this->where($attributes)->first())) {
538 4
            return $instance;
539
        }
540
541 4
        return $this->model->newInstance($attributes + $values);
542
    }
543
544 4
    public function firstOrCreate(array $attributes, array $values = [])
545
    {
546 4
        if (! is_null($instance = $this->where($attributes)->first())) {
547 2
            return $instance;
548
        }
549
550 2
        $newInstance = $this->model->newInstance($attributes + $values);
551 2
        $newInstance->save();
552 2
        return $newInstance;
553
    }
554
555 4
    public function updateOrCreate(array $attributes, array $values = [])
556
    {
557 4
        $instance = $this->firstOrNew($attributes);
558 4
        $instance->fill($values)->save();
559 4
        return $instance;
560
    }
561
    
562 5
    public function findOrFail($id, $columns = [])
563
    {
564 5
        $result = $this->find($id, $columns);
565
566 5
        if ($this->isMultipleIds($id)) {
567 1
            if (count($result) == count(array_unique($id))) {
0 ignored issues
show
Bug introduced by
$result of type BaoPham\DynamoDb\DynamoDbModel is incompatible with the type Countable|array expected by parameter $var of count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

567
            if (count(/** @scrutinizer ignore-type */ $result) == count(array_unique($id))) {
Loading history...
568 1
                return $result;
569
            }
570 4
        } elseif (! is_null($result)) {
571 2
            return $result;
572
        }
573
574 2
        throw (new ModelNotFoundException)->setModel(
575 2
            get_class($this->model),
576 2
            $id
577
        );
578
    }
579
580 28
    public function first($columns = [])
581
    {
582 28
        $items = $this->getAll($columns, 1);
583
584 28
        return $items->first();
585
    }
586
587 6
    public function firstOrFail($columns = [])
588
    {
589 6
        if (! is_null($model = $this->first($columns))) {
590 4
            return $model;
591
        }
592
593 2
        throw (new ModelNotFoundException)->setModel(get_class($this->model));
594
    }
595
596
    /**
597
     * Remove attributes from an existing item
598
     *
599
     * @param array ...$attributes
600
     * @return bool
601
     * @throws InvalidQuery
602
     */
603 6
    public function removeAttribute(...$attributes)
604
    {
605 6
        $keySet = !empty(array_filter($this->model->getKeys()));
606
607 6
        if (!$keySet) {
608 4
            $analyzer = $this->getConditionAnalyzer();
609
610 4
            if (!$analyzer->isExactSearch()) {
611
                throw new InvalidQuery('Need to provide the key in your query');
612
            }
613
614 4
            $id = $analyzer->identifierConditionValues();
615 4
            $this->model->setId($id);
616
        }
617
618 6
        $key = DynamoDb::marshalItem($this->model->getKeys());
619
620 6
        $this->resetExpressions();
621
622
        /** @var \Aws\Result $result */
623 6
        $result = DynamoDb::table($this->model->getTable())
624 6
            ->setKey($key)
625 6
            ->setUpdateExpression($this->updateExpression->remove($attributes))
626 6
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
627 6
            ->setReturnValues('ALL_NEW')
628 6
            ->prepare($this->client)
629 6
            ->updateItem();
630
631 6
        $success = array_get($result, '@metadata.statusCode') === 200;
632
633 6
        if ($success) {
634 6
            $this->model->setRawAttributes(DynamoDb::unmarshalItem($result->get('Attributes')));
635 6
            $this->model->syncOriginal();
636
        }
637
638 6
        return $success;
639
    }
640
641 2
    public function delete()
642
    {
643 2
        $result = DynamoDb::table($this->model->getTable())
644 2
            ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
645 2
            ->prepare($this->client)
646 2
            ->deleteItem();
647
648 2
        return array_get($result->toArray(), '@metadata.statusCode') === 200;
649
    }
650
651 2
    public function deleteAsync()
652
    {
653 2
        $promise = DynamoDb::table($this->model->getTable())
654 2
            ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
655 2
            ->prepare($this->client)
656 2
            ->deleteItemAsync();
657
658 2
        return $promise;
659
    }
660
661 14
    public function save()
662
    {
663 14
        $result = DynamoDb::table($this->model->getTable())
664 14
            ->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
665 14
            ->prepare($this->client)
666 14
            ->putItem();
667
668 14
        return array_get($result, '@metadata.statusCode') === 200;
669
    }
670
671 6
    public function saveAsync()
672
    {
673 6
        $promise = DynamoDb::table($this->model->getTable())
674 6
            ->setItem(DynamoDb::marshalItem($this->model->getAttributes()))
675 6
            ->prepare($this->client)
676 6
            ->putItemAsync();
677
678 6
        return $promise;
679
    }
680
681 55
    public function get($columns = [])
682
    {
683 55
        return $this->all($columns);
684
    }
685
686 67
    public function all($columns = [])
687
    {
688 67
        $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
689 67
        return $this->getAll($columns, $limit, !isset($this->limit));
690
    }
691
692 4
    public function count()
693
    {
694 4
        $limit = isset($this->limit) ? $this->limit : static::MAX_LIMIT;
695 4
        $raw = $this->toDynamoDbQuery(['count(*)'], $limit);
696
697 4
        if ($raw->op === 'Scan') {
698 4
            $res = $this->client->scan($raw->query);
699
        } else {
700
            $res = $this->client->query($raw->query);
701
        }
702
703 4
        return $res['Count'];
704
    }
705
706 4
    public function decorate(Closure $closure)
707
    {
708 4
        $this->decorator = $closure;
709 4
        return $this;
710
    }
711
712 100
    protected function getAll(
713
        $columns = [],
714
        $limit = DynamoDbQueryBuilder::MAX_LIMIT,
715
        $useIterator = DynamoDbQueryBuilder::DEFAULT_TO_ITERATOR
716
    ) {
717 100
        $analyzer = $this->getConditionAnalyzer();
718
719 100
        if ($analyzer->isExactSearch()) {
720 19
            $item = $this->find($analyzer->identifierConditionValues(), $columns);
721
722 19
            return $this->getModel()->newCollection([$item]);
723
        }
724
725 82
        $raw = $this->toDynamoDbQuery($columns, $limit);
726
727 82
        if ($useIterator) {
728 65
            $iterator = $this->client->getIterator($raw->op, $raw->query);
729
730 65
            if (isset($raw->query['Limit'])) {
731 12
                $iterator = new \LimitIterator($iterator, 0, $raw->query['Limit']);
732
            }
733
        } else {
734 21
            if ($raw->op === 'Scan') {
735 18
                $res = $this->client->scan($raw->query);
736
            } else {
737 3
                $res = $this->client->query($raw->query);
738
            }
739
740 21
            $this->lastEvaluatedKey = array_get($res, 'LastEvaluatedKey');
741 21
            $iterator = $res['Items'];
742
        }
743
744 82
        $results = [];
745
746 82
        foreach ($iterator as $item) {
747 82
            $item = DynamoDb::unmarshalItem($item);
748 82
            $model = $this->model->newInstance([], true);
749 82
            $model->setRawAttributes($item, true);
750 82
            $results[] = $model;
751
        }
752
753 82
        return $this->getModel()->newCollection($results, $analyzer->index());
754
    }
755
756
    /**
757
     * Return the raw DynamoDb query
758
     *
759
     * @param array $columns
760
     * @param int $limit
761
     * @return RawDynamoDbQuery
762
     */
763 92
    public function toDynamoDbQuery(
764
        $columns = [],
765
        $limit = DynamoDbQueryBuilder::MAX_LIMIT
766
    ) {
767 92
        $this->applyScopes();
768
769 92
        $this->resetExpressions();
770
771 92
        $op = 'Scan';
772 92
        $queryBuilder = DynamoDb::table($this->model->getTable());
773
774 92
        if (! empty($this->wheres)) {
775 70
            $analyzer = $this->getConditionAnalyzer();
776
777 70
            if ($keyConditions = $analyzer->keyConditions()) {
778 16
                $op = 'Query';
779 16
                $queryBuilder->setKeyConditionExpression($this->keyConditionExpression->parse($keyConditions));
780
            }
781
782 70
            if ($filterConditions = $analyzer->filterConditions()) {
783 61
                $queryBuilder->setFilterExpression($this->filterExpression->parse($filterConditions));
784
            }
785
786 70
            if ($index = $analyzer->index()) {
787 8
                $queryBuilder->setIndexName($index->name);
788
            }
789
        }
790
791 92
        if ($this->index) {
792
            // If user specifies the index manually, respect that
793 1
            $queryBuilder->setIndexName($this->index);
794
        }
795
796 92
        if ($limit !== static::MAX_LIMIT) {
797 33
            $queryBuilder->setLimit($limit);
798
        }
799
800 92
        if (!empty($columns)) {
801
            // Either we try to get the count or specific columns
802 8
            if ($columns == ['count(*)']) {
803 6
                $queryBuilder->setSelect('COUNT');
804
            } else {
805 2
                $queryBuilder->setProjectionExpression($this->projectionExpression->parse($columns));
806
            }
807
        }
808
809 92
        if (!empty($this->lastEvaluatedKey)) {
810 15
            $queryBuilder->setExclusiveStartKey($this->lastEvaluatedKey);
811
        }
812
813
        $queryBuilder
814 92
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
815 92
            ->setExpressionAttributeValues($this->expressionAttributeValues->all());
816
817 92
        $raw = new RawDynamoDbQuery($op, $queryBuilder->prepare($this->client)->query);
818
819 92
        if ($this->decorator) {
820 4
            call_user_func($this->decorator, $raw);
821
        }
822
823 92
        return $raw;
824
    }
825
826
    /**
827
     * @return Analyzer
828
     */
829 109
    protected function getConditionAnalyzer()
830
    {
831 109
        return with(new Analyzer)
832 109
            ->on($this->model)
833 109
            ->withIndex($this->index)
834 109
            ->analyze($this->wheres);
835
    }
836
837 52
    protected function isMultipleIds($id)
838
    {
839 52
        $keys = collect($this->model->getKeyNames());
840
841
        // could be ['id' => 'foo'], ['id1' => 'foo', 'id2' => 'bar']
842
        $single = $keys->first(function ($name) use ($id) {
843 52
            return !isset($id[$name]);
844 52
        }) === null;
845
846 52
        if ($single) {
847 34
            return false;
848
        }
849
850
        // could be ['foo', 'bar'], [['id1' => 'foo', 'id2' => 'bar'], ...]
851 18
        return $this->model->hasCompositeKey() ? is_array(H::array_first($id)) : is_array($id);
852
    }
853
854
    /**
855
     * @return DynamoDbModel
856
     */
857 100
    public function getModel()
858
    {
859 100
        return $this->model;
860
    }
861
862
    /**
863
     * @return \Aws\DynamoDb\DynamoDbClient
864
     */
865
    public function getClient()
866
    {
867
        return $this->client;
868
    }
869
870
    /**
871
     * Register a new global scope.
872
     *
873
     * @param  string  $identifier
874
     * @param  \Illuminate\Database\Eloquent\Scope|\Closure  $scope
875
     * @return $this
876
     */
877 7
    public function withGlobalScope($identifier, $scope)
878
    {
879 7
        $this->scopes[$identifier] = $scope;
880
881 7
        if (method_exists($scope, 'extend')) {
882
            $scope->extend($this);
883
        }
884
885 7
        return $this;
886
    }
887
888
    /**
889
     * Remove a registered global scope.
890
     *
891
     * @param  \Illuminate\Database\Eloquent\Scope|string  $scope
892
     * @return $this
893
     */
894 3
    public function withoutGlobalScope($scope)
895
    {
896 3
        if (! is_string($scope)) {
897
            $scope = get_class($scope);
898
        }
899
900 3
        unset($this->scopes[$scope]);
901
902 3
        $this->removedScopes[] = $scope;
903
904 3
        return $this;
905
    }
906
907
    /**
908
     * Remove all or passed registered global scopes.
909
     *
910
     * @param  array|null  $scopes
911
     * @return $this
912
     */
913 5
    public function withoutGlobalScopes(array $scopes = null)
914
    {
915 5
        if (is_array($scopes)) {
916
            foreach ($scopes as $scope) {
917
                $this->withoutGlobalScope($scope);
918
            }
919
        } else {
920 5
            $this->scopes = [];
921
        }
922
923 5
        return $this;
924
    }
925
926
    /**
927
     * Get an array of global scopes that were removed from the query.
928
     *
929
     * @return array
930
     */
931
    public function removedScopes()
932
    {
933
        return $this->removedScopes;
934
    }
935
936
    /**
937
     * Apply the scopes to the Eloquent builder instance and return it.
938
     *
939
     * @return DynamoDbQueryBuilder
940
     */
941 92
    public function applyScopes()
942
    {
943 92
        if (! $this->scopes) {
944 91
            return $this;
945
        }
946
947 3
        $builder = $this;
948
949 3
        foreach ($builder->scopes as $identifier => $scope) {
950 3
            if (! isset($builder->scopes[$identifier])) {
951
                continue;
952
            }
953
954
            $builder->callScope(function (DynamoDbQueryBuilder $builder) use ($scope) {
955
                // If the scope is a Closure we will just go ahead and call the scope with the
956
                // builder instance. The "callScope" method will properly group the clauses
957
                // that are added to this query so "where" clauses maintain proper logic.
958 3
                if ($scope instanceof Closure) {
959 3
                    $scope($builder);
960
                }
961
962
                // If the scope is a scope object, we will call the apply method on this scope
963
                // passing in the builder and the model instance. After we run all of these
964
                // scopes we will return back the builder instance to the outside caller.
965 3
                if ($scope instanceof Scope) {
966
                    throw new NotSupportedException('Scope object is not yet supported');
967
                }
968 3
            });
969
970 3
            $builder->withoutGlobalScope($identifier);
971
        }
972
973 3
        return $builder;
974
    }
975
976
    /**
977
     * Apply the given scope on the current builder instance.
978
     *
979
     * @param  callable  $scope
980
     * @param  array  $parameters
981
     * @return mixed
982
     */
983 7
    protected function callScope(callable $scope, $parameters = [])
984
    {
985 7
        array_unshift($parameters, $this);
986
987
        // $query = $this->getQuery();
988
989
        // // We will keep track of how many wheres are on the query before running the
990
        // // scope so that we can properly group the added scope constraints in the
991
        // // query as their own isolated nested where statement and avoid issues.
992
        // $originalWhereCount = is_null($query->wheres)
993
        //             ? 0 : count($query->wheres);
994
995 7
        $result = $scope(...array_values($parameters)) ?: $this;
996
997
        // if (count((array) $query->wheres) > $originalWhereCount) {
998
        //     $this->addNewWheresWithinGroup($query, $originalWhereCount);
999
        // }
1000
1001 7
        return $result;
1002
    }
1003
1004
    /**
1005
     * Dynamically handle calls into the query instance.
1006
     *
1007
     * @param  string  $method
1008
     * @param  array  $parameters
1009
     * @return mixed
1010
     */
1011 7
    public function __call($method, $parameters)
1012
    {
1013 7
        if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
1014 5
            return $this->callScope([$this->model, $scope], $parameters);
1015
        }
1016
1017 2
        return $this;
1018
    }
1019
}
1020