Completed
Pull Request — master (#241)
by Caleb
13:04
created

DynamoDbQueryBuilder::firstOrNew()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

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