Test Failed
Pull Request — master (#179)
by
unknown
03:44
created

DynamoDbQueryBuilder::firstOrCreate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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 132
    public function __construct(DynamoDbModel $model)
79
    {
80 132
        $this->model = $model;
81 132
        $this->client = $model->getClient();
82 132
        $this->setupExpressions();
83 132
    }
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 74
    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 74
        if (is_array($column)) {
213
            foreach ($column as $key => $value) {
214
                return $this->where($key, '=', $value);
215
            }
216
        }
217
218
        // Here we will make some assumptions about the operator. If only 2 values are
219
        // passed to the method, we will assume that the operator is an equals sign
220
        // and keep going. Otherwise, we'll require the operator to be passed in.
221 74
        if (func_num_args() == 2) {
222 45
            list($value, $operator) = [$operator, '='];
223
        }
224
225
        // If the columns is actually a Closure instance, we will assume the developer
226
        // wants to begin a nested where statement which is wrapped in parenthesis.
227
        // We'll add that Closure to the query then return back out immediately.
228 74
        if ($column instanceof Closure) {
229 2
            return $this->whereNested($column, $boolean);
230
        }
231
232
        // If the given operator is not found in the list of valid operators we will
233
        // assume that the developer is just short-cutting the '=' operators and
234
        // we will set the operators to '=' and set the values appropriately.
235 74
        if (!ComparisonOperator::isValidOperator($operator)) {
236 4
            list($value, $operator) = [$operator, '='];
237
        }
238
239
        // If the value is a Closure, it means the developer is performing an entire
240
        // sub-select within the query and we will need to compile the sub-select
241
        // within the where clause to get the appropriate query record results.
242 74
        if ($value instanceof Closure) {
243
            throw new NotSupportedException('Closure in where clause is not supported');
244
        }
245
246 74
        $this->wheres[] = [
247 74
            'column' => $column,
248 74
            'type' => ComparisonOperator::getDynamoDbOperator($operator),
249 74
            'value' => $value,
250 74
            'boolean' => $boolean,
251
        ];
252
253 74
        return $this;
254
    }
255
256
    /**
257
     * Add a nested where statement to the query.
258
     *
259
     * @param  \Closure $callback
260
     * @param  string   $boolean
261
     * @return $this
262
     */
263 2
    public function whereNested(Closure $callback, $boolean = 'and')
264
    {
265 2
        call_user_func($callback, $query = $this->forNestedWhere());
266
267 2
        return $this->addNestedWhereQuery($query, $boolean);
268
    }
269
270
    /**
271
     * Create a new query instance for nested where condition.
272
     *
273
     * @return $this
274
     */
275 2
    public function forNestedWhere()
276
    {
277 2
        return $this->newQuery();
278
    }
279
280
    /**
281
     * Add another query builder as a nested where to the query builder.
282
     *
283
     * @param  DynamoDbQueryBuilder $query
284
     * @param  string  $boolean
285
     * @return $this
286
     */
287 2
    public function addNestedWhereQuery($query, $boolean = 'and')
288
    {
289 2
        if (count($query->wheres)) {
290 2
            $type = 'Nested';
291 2
            $column = null;
292 2
            $value = $query->wheres;
293 2
            $this->wheres[] = compact('column', 'type', 'value', 'boolean');
294
        }
295
296 2
        return $this;
297
    }
298
299
    /**
300
     * Add an "or where" clause to the query.
301
     *
302
     * @param  string  $column
303
     * @param  string  $operator
304
     * @param  mixed   $value
305
     * @return $this
306
     */
307 22
    public function orWhere($column, $operator = null, $value = null)
308
    {
309 22
        return $this->where($column, $operator, $value, 'or');
310
    }
311
312
    /**
313
     * Add a "where in" clause to the query.
314
     *
315
     * @param  string  $column
316
     * @param  mixed   $values
317
     * @param  string  $boolean
318
     * @param  bool    $not
319
     * @return $this
320
     * @throws NotSupportedException
321
     */
322 2
    public function whereIn($column, $values, $boolean = 'and', $not = false)
323
    {
324 2
        if ($not) {
325
            throw new NotSupportedException('"not in" is not a valid DynamoDB comparison operator');
326
        }
327
328
        // If the value is a query builder instance, not supported
329 2
        if ($values instanceof static) {
330
            throw new NotSupportedException('Value is a query builder instance');
331
        }
332
333
        // If the value of the where in clause is actually a Closure, not supported
334 2
        if ($values instanceof Closure) {
335
            throw new NotSupportedException('Value is a Closure');
336
        }
337
338
        // Next, if the value is Arrayable we need to cast it to its raw array form
339 2
        if ($values instanceof Arrayable) {
340
            $values = $values->toArray();
341
        }
342
343 2
        return $this->where($column, ComparisonOperator::IN, $values, $boolean);
344
    }
345
346
    /**
347
     * Add an "or where in" clause to the query.
348
     *
349
     * @param  string  $column
350
     * @param  mixed   $values
351
     * @return $this
352
     */
353 2
    public function orWhereIn($column, $values)
354
    {
355 2
        return $this->whereIn($column, $values, 'or');
356
    }
357
358
    /**
359
     * Add a "where null" clause to the query.
360
     *
361
     * @param  string  $column
362
     * @param  string  $boolean
363
     * @param  bool    $not
364
     * @return $this
365
     */
366 4
    public function whereNull($column, $boolean = 'and', $not = false)
367
    {
368 4
        $type = $not ? ComparisonOperator::NOT_NULL : ComparisonOperator::NULL;
369
370 4
        $this->wheres[] = compact('column', 'type', 'boolean');
371
372 4
        return $this;
373
    }
374
375
    /**
376
     * Add an "or where null" clause to the query.
377
     *
378
     * @param  string  $column
379
     * @return $this
380
     */
381 2
    public function orWhereNull($column)
382
    {
383 2
        return $this->whereNull($column, 'or');
384
    }
385
386
    /**
387
     * Add an "or where not null" clause to the query.
388
     *
389
     * @param  string  $column
390
     * @return $this
391
     */
392 2
    public function orWhereNotNull($column)
393
    {
394 2
        return $this->whereNotNull($column, 'or');
395
    }
396
397
    /**
398
     * Add a "where not null" clause to the query.
399
     *
400
     * @param  string  $column
401
     * @param  string  $boolean
402
     * @return $this
403
     */
404 2
    public function whereNotNull($column, $boolean = 'and')
405
    {
406 2
        return $this->whereNull($column, $boolean, true);
407
    }
408
409
    /**
410
     * Get a new instance of the query builder.
411
     *
412
     * @return DynamoDbQueryBuilder
413
     */
414 2
    public function newQuery()
415
    {
416 2
        return new static($this->getModel());
417
    }
418
419
    /**
420
     * Implements the Query Chunk method
421
     *
422
     * @param int $chunkSize
423
     * @param callable $callback
424
     */
425 9
    public function chunk($chunkSize, callable $callback)
426
    {
427 9
        while (true) {
428 9
            $results = $this->getAll([], $chunkSize, false);
429
430 9
            if ($results->isNotEmpty()) {
431 9
                if (call_user_func($callback, $results) === false) {
432 2
                    return false;
433
                }
434
            }
435
436 7
            if (empty($this->lastEvaluatedKey)) {
437 7
                break;
438
            }
439
        }
440
441 7
        return true;
442
    }
443
444
    /**
445
     * @param $id
446
     * @param array $columns
447
     * @return DynamoDbModel|\Illuminate\Database\Eloquent\Collection|null
448
     */
449 43
    public function find($id, array $columns = [])
450
    {
451 43
        if ($this->isMultipleIds($id)) {
452 4
            return $this->findMany($id, $columns);
453
        }
454
455 39
        $this->resetExpressions();
456
457 39
        $this->model->setId($id);
458
459 39
        $query = DynamoDb::table($this->model->getTable())
460 39
            ->setKey(DynamoDb::marshalItem($this->model->getKeys()))
461 39
            ->setConsistentRead(true);
462
463 39
        if (!empty($columns)) {
464
            $query
465 3
                ->setProjectionExpression($this->projectionExpression->parse($columns))
466 3
                ->setExpressionAttributeNames($this->expressionAttributeNames->all());
467
        }
468
469 39
        $item = $query->prepare($this->client)->getItem();
470
471 39
        $item = array_get($item->toArray(), 'Item');
472
473 39
        if (empty($item)) {
474 6
            return null;
475
        }
476
477 35
        $item = DynamoDb::unmarshalItem($item);
478
479 35
        $model = $this->model->newInstance([], true);
480
481 35
        $model->setRawAttributes($item, true);
482
483 35
        return $model;
484
    }
485
486
    /**
487
     * @param $ids
488
     * @param array $columns
489
     * @return \Illuminate\Database\Eloquent\Collection
490
     */
491 4
    public function findMany($ids, array $columns = [])
492
    {
493 4
        $collection = $this->model->newCollection();
494
495 4
        if (empty($ids)) {
496
            return $collection;
497
        }
498
499 4
        $this->resetExpressions();
500
501 4
        $table = $this->model->getTable();
502
503
        $keys = collect($ids)->map(function ($id) {
504 4
            if (! is_array($id)) {
505 2
                $id = [$this->model->getKeyName() => $id];
506
            }
507
508 4
            return DynamoDb::marshalItem($id);
509 4
        });
510
511 4
        $subQuery = DynamoDb::newQuery()
512 4
            ->setKeys($keys->toArray())
513 4
            ->setProjectionExpression($this->projectionExpression->parse($columns))
514 4
            ->setExpressionAttributeNames($this->expressionAttributeNames->all())
515 4
            ->prepare($this->client)
516 4
            ->query;
517
518 4
        $results = DynamoDb::newQuery()
519 4
            ->setRequestItems([$table => $subQuery])
520 4
            ->prepare($this->client)
521 4
            ->batchGetItem();
522
523 4
        foreach ($results['Responses'][$table] as $item) {
524 4
            $item = DynamoDb::unmarshalItem($item);
525 4
            $model = $this->model->newInstance([], true);
526 4
            $model->setRawAttributes($item, true);
527 4
            $collection->add($model);
528
        }
529
530 4
        return $collection;
531
    }
532
533 3
    public function firstOrNew($id, $attributes = [])
534
    {
535 3
        $result = $this->find($id);
536
537 3
        if ($this->isMultipleIds($id)) {
538
            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

538
            if (count(/** @scrutinizer ignore-type */ $result) == count(array_unique($id))) {
Loading history...
539
                return $result;
540
            }
541 3
        } elseif (!is_null($result)) {
542 3
            return $result;
543
        }
544
545 2
        $model = $this->model->newInstance($attributes);
546 2
        $model->setId($id);
547
548 2
        return $model;
549
    }
550
551 1
    public function firstOrCreate($id, $attributes = [])
552
    {
553 1
        $model = $this->firstOrNew($id, $attributes);
554 1
        $model->save();
555
556 1
        return $model;
557
    }
558
559 1
    public function updateOrCreate($id, $attributes = [])
560
    {
561 1
        $model = $this->firstOrNew($id, $attributes);
562 1
        $model->fill($attributes);
563 1
        $model->save();
564
565 1
        return $model;
566
    }
567
    
568 5
    public function findOrFail($id, $columns = [])
569
    {
570 5
        $result = $this->find($id, $columns);
571
572 5
        if ($this->isMultipleIds($id)) {
573 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

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