<?php

namespace ostark\Yii2ArtisanBridge\base;

use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use yii\base\Arrayable;

/**
 * Trait ArtisanOutputTrait
 *
 * Borrowed from Illuminate\Console\Command
 * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Console/Command.php
 *
 * @property \ostark\Yii2ArtisanBridge\ConsoleOutput $input
 * @property \ostark\Yii2ArtisanBridge\OutputStyle  $output
 *
 */
trait ArtisanOutputTrait
{

    /**
     * The default verbosity of output commands.
     *
     * @var int
     */
    protected $verbosity = OutputInterface::VERBOSITY_NORMAL;
    /**
     * The mapping between human readable verbosity levels and Symfony's OutputInterface.
     *
     * @var array
     */
    protected $verbosityMap = [
        'v' => OutputInterface::VERBOSITY_VERBOSE,
        'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE,
        'vvv' => OutputInterface::VERBOSITY_DEBUG,
        'quiet' => OutputInterface::VERBOSITY_QUIET,
        'normal' => OutputInterface::VERBOSITY_NORMAL,
    ];

    /**
     * Confirm a question with the user.
     *
     * @param  string  $question
     * @param  bool    $default
     * @return bool
     */
    public function confirm($question, $default = false)
    {
        return $this->output->confirm($question, $default);
    }

    /**
     * Prompt the user for input.
     *
     * @param  string      $question
     * @param  string|null $default
     *
     * @return string
     */
    public function ask($question, $default = null)
    {
        return $this->output->ask($question, $default);
    }

    /**
     * Prompt the user for input with auto completion.
     *
     * @param  string      $question
     * @param  array       $choices
     * @param  string|null $default
     *
     * @return string
     */
    public function anticipate($question, array $choices, $default = null)
    {
        return $this->askWithCompletion($question, $choices, $default);
    }

    /**
     * Prompt the user for input with auto completion.
     *
     * @param  string      $question
     * @param  array       $choices
     * @param  string|null $default
     *
     * @return string
     */
    public function askWithCompletion($question, array $choices, $default = null)
    {
        $question = new Question($question, $default);
        $question->setAutocompleterValues($choices);

        return $this->output->askQuestion($question);
    }

    /**
     * Prompt the user for input but hide the answer from the console.
     *
     * @param  string $question
     * @param  bool   $fallback
     *
     * @return string
     */
    public function secret($question, $fallback = true)
    {
        $question = new Question($question);
        $question->setHidden(true)->setHiddenFallback($fallback);

        return $this->output->askQuestion($question);
    }

    /**
     * Give the user a single choice from an array of answers.
     *
     * @param  string      $question
     * @param  array       $choices
     * @param  string|null $default
     * @param  mixed|null  $attempts
     * @param  bool|null   $multiple
     *
     * @return string
     */
    public function choice($question, array $choices, $default = null, $attempts = null, $multiple = null)
    {
        $question = new ChoiceQuestion($question, $choices, $default);
        $question->setMaxAttempts($attempts)->setMultiselect($multiple);

        return $this->output->askQuestion($question);
    }

    /**
     * Format input to textual table.
     *
     * @param        $headers
     * @param        $rows
     * @param string $tableStyle
     * @param array  $columnStyles
     */
    public function table($headers, $rows, $tableStyle = 'default', array $columnStyles = [])
    {
        $table = new Table($this->output);
        if ($rows instanceof Arrayable) {
            $rows = $rows->toArray();
        }
        $table->setHeaders((array)$headers)->setRows($rows)->setStyle($tableStyle);
        foreach ($columnStyles as $columnIndex => $columnStyle) {
            $table->setColumnStyle($columnIndex, $columnStyle);
        }
        $table->render();
    }

    /**
     * Write a string as information output.
     *
     * @param  string          $string
     * @param  null|int|string $verbosity
     *
     * @return void
     */
    public function info($string, $verbosity = null)
    {
        $this->line($string, 'info', $verbosity);
    }

    /**
     * Write a string as standard output.
     *
     * @param  string          $string
     * @param  string          $style
     * @param  null|int|string $verbosity
     *
     * @return void
     */
    public function line($string, $style = null, $verbosity = null)
    {
        $styled = $style ? "<$style>$string</$style>" : $string;
        $this->output->writeln($styled, $this->parseVerbosity($verbosity));
    }

    /**
     * Write a string as comment output.
     *
     * @param  string          $string
     * @param  null|int|string $verbosity
     *
     * @return void
     */
    public function comment($string, $verbosity = null)
    {
        $this->line($string, 'comment', $verbosity);
    }

    /**
     * Write a string as question output.
     *
     * @param  string          $string
     * @param  null|int|string $verbosity
     *
     * @return void
     */
    public function question($string, $verbosity = null)
    {
        $this->line($string, 'question', $verbosity);
    }

    /**
     * Write a string as error output.
     *
     * @param  string          $string
     * @param  null|int|string $verbosity
     *
     * @return void
     */
    public function error($string, $verbosity = null)
    {
        $this->line($string, 'error', $verbosity);
    }

    /**
     * Write a string as warning output.
     *
     * @param  string          $string
     * @param  null|int|string $verbosity
     *
     * @return void
     */
    public function warn($string, $verbosity = null)
    {
        if (!$this->output->getFormatter()->hasStyle('warning')) {
            $style = new OutputFormatterStyle('yellow');
            $this->output->getFormatter()->setStyle('warning', $style);
        }
        $this->line($string, 'warning', $verbosity);
    }

    /**
     * Write a string in an alert box.
     *
     * @param  string $string
     *
     * @return void
     */
    public function alert($string)
    {
        $this->comment(str_repeat('*', strlen($string) + 12));
        $this->comment('*     ' . $string . '     *');
        $this->comment(str_repeat('*', strlen($string) + 12));
        $this->output->newLine();
    }

    /**
     * Set the verbosity level.
     *
     * @param  string|int $level
     *
     * @return void
     */
    protected function setVerbosity($level)
    {
        $this->verbosity = $this->parseVerbosity($level);
    }

    /**
     * Get the verbosity level in terms of Symfony's OutputInterface level.
     *
     * @param  string|int|null $level
     *
     * @return int
     */
    protected function parseVerbosity($level = null)
    {
        if (isset($this->verbosityMap[$level])) {
            $level = $this->verbosityMap[$level];
        } elseif (!is_int($level)) {
            $level = $this->verbosity;
        }

        return $level;
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return [];
    }

    /**
     * Get the output implementation.
     *
     * @return \Symfony\Component\Console\Output\OutputInterface
     */
    public function getOutput()
    {
        return $this->output;
    }
}


<?php

/**
 * @copyright   Copyright (c) 2015 ublaboo <ublaboo@paveljanda.com>
 * @author      Pavel Janda <me@paveljanda.com>
 * @package     Ublaboo
 */

namespace Ublaboo\Controls\Controls;

use Nette;

class TextInputCustomLabel extends Nette\Forms\Controls\TextInput
{

	/**
	 * Register TextInputCustomLabel
	 * @param  string $control_name string
	 * @return void
	 */
	public static function register($control_name = 'addTextCustomLabel') {
		Nette\Object::extensionMethod(
			'Nette\Forms\Container::' . $control_name,
			function ($form, $name, $label = NULL, array $items = NULL) {
				$control = new self($label, $items);

				return $form[$name] = $control;
			}
		);
	}

	/**
	 * Generates label's HTML element.
	 * @param  string
	 * @return Html|string
	 */
	public function getLabel($caption = NULL)
	{
		$label = clone $this->label;
		$label->for = $this->getHtmlId();

		if (!$label->getHtml()) {
			$label->setText($this->translate($caption === NULL ? $this->caption : $caption));
		}

		return $label;
	}

}


<?php

namespace VergilLai\NodeCategories;

use DB;
use Illuminate\Database\Eloquent\Model;

/**
 * NodeCategoryTrait
 *
 * @property integer $id
 * @property integer $parent_id
 * @property boolean $level
 * @property string $name
 * @property string $node
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @package VergilLai\NodeCategories
 * @author Vergil <vergil@vip.163.com>
 */
trait NodeCategoryTrait
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function parent()
    {
        return $this->belongsTo(self::class, 'parent_id', 'id');
    }

    public function getParent()
    {
        return $this->findOrFail($this->parent_id);
    }

    /**
     * Get all children node
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function childrens()
    {
        return $this->where('node', 'like', $this->node.'%')->where('id', '<>', $this->id)->get();
    }

    /**
     * Get all parent node
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function parents()
    {
        return $this->where(DB::raw("LOCATE(`node`, '{$this->node}')"), '>', 0)
            ->where('id', '<>', $this->id)->get();
    }

    /**
     * Get all category name
     *
     * @param string $delimiter
     * @return string
     */
    public function fullPathName($delimiter = '/')
    {
        $parents = $this->parents();

        return implode($delimiter, $parents->pluck('name')->toArray()) . $delimiter . $this->name;
    }

    /**
     * Get tree structures
     * @param callable $map
     * @return array
     */
    public static function getTree(\Closure $map = null)
    {
        return static::makeTree((new static)->get(), $map);
    }

    /**
     * Get tree structures
     * @param \Illuminate\Database\Eloquent\Collection $collection
     * @param callable $map
     * @return array
     */
    public static function makeTree(\Illuminate\Database\Eloquent\Collection $collection, \Closure $map = null)
    {
        if (is_callable($map)) {
            $collection->map($map);
        }

        $categories = $collection->keyBy('id')->toArray();

        $nodesKey = (new static)->getNodesKey();

        foreach($categories as $cate) {
            $categories[$cate['parent_id']][$nodesKey][] = & $categories[$cate['id']];
        }

        return isset($categories[0][$nodesKey]) ? $categories[0][$nodesKey] : [];
    }

    /**
     * Get the tree structures nodes key name.
     * @return string
     */
    protected function getNodesKey()
    {
        return property_exists($this, 'nodesKey') ? $this->nodesKey : 'nodes';
    }
}


<?php

namespace ostark\Yii2ArtisanBridge;

use Symfony\Component\Console\Style\SymfonyStyle;

class OutputStyle extends SymfonyStyle
{

    /**
     * Emulate terminal typing
     *
     * @param string $command
     * @param string $style
     * @param int    $speed
     * @param string|null $prepend
     */
    public function type(string $command, string $style = "fg=white", $speed = 5, string $prepend = "$ ")
    {
        if (!is_null($prepend)) {
            $this->write($prepend);
        }

        $chars = str_split($command);

        foreach ($chars as $char) {

            $this->write(sprintf("<%s>%s</>", $style, $char));

            if (is_int($speed) && $speed > 0) {
                $delay = rand(10000, 1000000) / $speed;
                usleep($delay);
            }
        }

        // delay before new line
        usleep(1000000 / $speed);
        $this->newLine();
    }
}


<?php
namespace DreamFactory\Core\Aws\Resources;

use DreamFactory\Core\Resources\BaseRestResource;
use DreamFactory\Core\Aws\Services\Sns;

class BaseSnsResource extends BaseRestResource
{
    //*************************************************************************
    //	Constants
    //*************************************************************************

    //*************************************************************************
    //	Members
    //*************************************************************************

    /**
     * @var null|Sns
     */
    protected $service = null;

    /**
     * @var null|string
     */
    protected $parentResource = null;

    //*************************************************************************
    //	Methods
    //*************************************************************************

    /**
     * @param Sns   $service
     * @param array $settings
     */
    public function __construct($service = null, $settings = array())
    {
        parent::__construct($settings);

        $this->service = $service;
    }

    /**
     * @param Sns|null $service
     */
    public function setService($service)
    {
        $this->service = $service;
    }

    /**
     * @param null|string $parentResource
     *
     * @return $this
     */
    public function setParentResource($parentResource)
    {
        if (!empty($parentResource)) {
            $parentResource = $this->service->addArnPrefix($parentResource);
        }
        $this->parentResource = $parentResource;

        return $this;
    }
}


<?php
namespace DreamFactory\Core\Aws\Resources;

use DreamFactory\Core\Aws\Services\Sns;
use DreamFactory\Core\Exceptions\BadRequestException;
use DreamFactory\Core\Exceptions\InternalServerErrorException;
use DreamFactory\Core\Exceptions\NotFoundException;
use DreamFactory\Core\Contracts\ServiceResponseInterface;

/**
 * Class SnsEndpoint
 *
 * @package DreamFactory\Core\Aws\Resources
 */
class SnsEndpoint extends BaseSnsResource
{
    //*************************************************************************
    //	Constants
    //*************************************************************************

    /**
     * Resource tag for dealing with subscription
     */
    const RESOURCE_NAME = 'endpoint';

    //*************************************************************************
    //	Members
    //*************************************************************************

    //*************************************************************************
    //	Methods
    //*************************************************************************

    /**
     * {@inheritdoc}
     */
    protected static function getResourceIdentifier()
    {
        return 'EndpointArn';
    }

    /**
     * @return array
     * @throws BadRequestException
     * @throws InternalServerErrorException
     * @throws NotFoundException
     * @throws null
     */
    protected function getEndpointsAsArray($application)
    {
        if (empty($application)) {
            throw new BadRequestException('Platform application name required for retrieving endpoints.');
        }

        $application = $this->service->addArnPrefix($application);
        $out = [];
        $token = null;
        try {
            do {
                $result = $this->service->getConnection()->listEndpointsByPlatformApplication(
                    [
                        'PlatformApplicationArn' => $application,
                        'NextToken'              => $token
                    ]
                );
                $topics = $result['Endpoints'];
                $token = $result['NextToken'];

                if (!empty($topics)) {
                    $out = array_merge($out, $topics);
                }
            } while ($token);
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to retrieve endpoints.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return $out;
    }

    /**
     * Apply the commonly used REST path members to the class.
     *
     * @param string $resourcePath
     *
     * @return $this
     */
    protected function setResourceMembers($resourcePath = null)
    {
        parent::setResourceMembers($resourcePath);

        $this->resource = array_get($this->resourceArray, 0);

        $pos = 1;
        $more = array_get($this->resourceArray, $pos);

        if (!empty($more)) {
            //  This will be the full resource path
            do {
                $this->resource .= '/' . $more;
                $pos++;
                $more = array_get($this->resourceArray, $pos);
            } while (!empty($more));
        }

        return $this;
    }

    /** {@inheritdoc} */
    public function getResources()
    {
        if(!empty($this->parentResource)) {
            return $this->getEndpointsAsArray($this->parentResource);
        } else {
            $resources = $this->listResources(Sns::FORMAT_ARN);
            foreach ($resources as $key => $value){
                $resources[$key] = ['EndpointArn' => $value];
            }

            return $resources;
        }
    }

    /**
     * @param mixed $fields Use boolean, comma-delimited string, or array of properties
     *
     * @return ServiceResponseInterface
     * @throws BadRequestException|InternalServerErrorException|NotFoundException
     */
    public function listResources($fields = null)
    {
        $resources = [];
        if (empty($this->parentResource)) {
            $applications = [];
            try {
                $out = [];
                $token = null;
                do {
                    $result = $this->service->getConnection()->listPlatformApplications(
                        [
                            'NextToken' => $token
                        ]
                    );
                    $topics = $result['PlatformApplications'];
                    $token = $result['NextToken'];

                    if (!empty($topics)) {
                        $out = array_merge($out, $topics);
                    }
                } while ($token);

                foreach ($out as $app) {
                    $applications[] = array_get($app, 'PlatformApplicationArn');
                }
            } catch (\Exception $ex) {
                if (null !== $newEx = Sns::translateException($ex)) {
                    throw $newEx;
                }

                throw new InternalServerErrorException("Failed to retrieve applications.\n{$ex->getMessage()}",
                    $ex->getCode());
            }
        } else {
            $applications = [$this->parentResource];
        }

        foreach ($applications as $application) {
            $result = $this->getEndpointsAsArray($application);
            foreach ($result as $end) {
                switch ($fields) {
                    case false:
                    case Sns::FORMAT_SIMPLE:
                        $resources[] = $this->service->stripArnPrefix(array_get($end, 'EndpointArn'));
                        break;
                    case Sns::FORMAT_ARN:
                        $resources[] = array_get($end, 'EndpointArn');
                        break;
                    case true:
                    case Sns::FORMAT_FULL:
                    default:
                        $end['Endpoint'] = $this->service->stripArnPrefix(array_get($end, 'EndpointArn'));
                        $resources[] = $end;
                        break;
                }
            }
        }

        return $resources;
    }

    /** {@inheritdoc} */
    protected function handleGET()
    {
        if (empty($this->resource)) {
            return parent::handleGET();
        } else {
            return $this->retrieveEndpoint($this->resource);
        }
    }

    /** {@inheritdoc} */
    protected function handlePOST()
    {
        $payload = $this->request->getPayloadData();
        if (empty($payload)) {
            throw new BadRequestException('No data in endpoint post request.');
        }

        if (empty($this->resource)) {
            return $this->createEndpoint($payload);
        } else {
            return $this->service->publish($payload, SnsEndpoint::RESOURCE_NAME, $this->resource);
        }
    }

    /** {@inheritdoc} */
    protected function handlePUT()
    {
        $payload = $this->request->getPayloadData();
        if (empty($payload)) {
            throw new BadRequestException('No data in endpoint update request.');
        }

        if (!empty($this->resource)) {
            $payload['Topic'] = $this->resource;
        }

        return $this->updateEndpoint($payload);
    }

    /** {@inheritdoc} */
    protected function handlePATCH()
    {
        return $this->handlePUT();
    }

    /** {@inheritdoc} */
    protected function handleDELETE()
    {
        $payload = $this->request->getPayloadData();
        if (empty($this->resource)) {
            if (empty($payload)) {
                throw new BadRequestException('No data in endpoint delete request.');
            }

            $this->deleteEndpoint($payload);
        } else {
            $this->deleteEndpoint($this->resource);
        }

        return ['success' => true];
    }

    /**
     * @param $resource
     *
     * @return array
     * @throws InternalServerErrorException
     * @throws NotFoundException
     */
    public function retrieveEndpoint($resource)
    {
        $request = ['EndpointArn' => $this->service->addArnPrefix($resource)];

        try {
            if (null !== $result = $this->service->getConnection()->getEndpointAttributes($request)) {
                $attributes = array_get($result->toArray(), 'Attributes');

                return [
                    'Endpoint'    => $this->service->stripArnPrefix($resource),
                    'EndpointArn' => $this->service->addArnPrefix($resource),
                    'Attributes'  => $attributes
                ];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to retrieve properties for '$resource'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }

    /**
     * @param $request
     *
     * @return array
     * @throws \DreamFactory\Core\Exceptions\BadRequestException
     * @throws \DreamFactory\Core\Exceptions\InternalServerErrorException
     */
    public function createEndpoint($request)
    {
        if (is_array($request)) {
            $name = array_get($request, 'Application', array_get($request, 'PlatformApplicationArn'));
            if (empty($name)) {
                throw new BadRequestException("Create endpoint request contains no 'Application' field.");
            }
            $request['PlatformApplicationArn'] = $this->service->addArnPrefix($name);
            $name = array_get($request, 'Token');
            if (empty($name)) {
                throw new BadRequestException("Create endpoint request contains no 'Token' field.");
            }
        } else {
            throw new BadRequestException("Create endpoint request contains fields.");
        }

        try {
            if (null !== $result = $this->service->getConnection()->createPlatformEndpoint($request)) {
                $arn = array_get($result->toArray(), 'EndpointArn', '');

                return ['Endpoint' => $this->service->stripArnPrefix($arn), 'EndpointArn' => $arn];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException(
                "Failed to create endpoint for '{$request['PlatformApplicationArn']}'.\n{$ex->getMessage()}",
                $ex->getCode()
            );
        }

        return [];
    }

    /**
     * @param $request
     *
     * @return array
     * @throws \DreamFactory\Core\Exceptions\BadRequestException
     * @throws \DreamFactory\Core\Exceptions\InternalServerErrorException
     */
    public function updateEndpoint($request)
    {
        if (is_array($request)) {
            $name = array_get($request, 'Endpoint', array_get($request, 'EndpointArn'));
            if (empty($name)) {
                throw new BadRequestException("Update endpoint request contains no 'Endpoint' field.");
            }

            $request['EndpointArn'] = $this->service->addArnPrefix($name);
        } else {
            throw new BadRequestException("Update endpoint request contains no fields.");
        }

        try {
            if (null !== $result = $this->service->getConnection()->setEndpointAttributes($request)) {
                return ['success' => true];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to update endpoint '{$request['EndpointArn']}'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }

    /**
     * @param $request
     *
     * @return array
     * @throws \DreamFactory\Core\Exceptions\BadRequestException
     * @throws \DreamFactory\Core\Exceptions\InternalServerErrorException
     */
    public function deleteEndpoint($request)
    {
        $data = [];
        if (is_array($request)) {
            $name = array_get($request, 'Endpoint', array_get($request, 'EndpointArn'));
            if (empty($name)) {
                throw new BadRequestException("Delete endpoint request contains no 'Endpoint' field.");
            }

            $data['EndpointArn'] = $this->service->addArnPrefix($name);
        } else {
            $data['EndpointArn'] = $this->service->addArnPrefix($request);
        }

        try {
            if (null !== $result = $this->service->getConnection()->deleteEndpoint($data)) {
                return ['success' => true];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to delete endpoint '{$data['EndpointArn']}'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }
}


<?php
namespace DreamFactory\Core\Aws\Resources;

use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\Marshaler;
use DreamFactory\Core\Aws\Enums\ReturnValue;
use DreamFactory\Core\Aws\Enums\Type;
use DreamFactory\Core\Database\Schema\ColumnSchema;
use DreamFactory\Core\Enums\ApiOptions;
use DreamFactory\Core\Database\Resources\BaseNoSqlDbTableResource;
use DreamFactory\Core\Enums\DbComparisonOperators;
use DreamFactory\Core\Enums\DbLogicalOperators;
use DreamFactory\Core\Enums\DbSimpleTypes;
use DreamFactory\Core\Utility\Session;
use DreamFactory\Core\Exceptions\BadRequestException;
use DreamFactory\Core\Exceptions\InternalServerErrorException;
use DreamFactory\Core\Exceptions\NotFoundException;
use DreamFactory\Core\Aws\Services\DynamoDb;
use DreamFactory\Core\Enums\Verbs;

class DynamoDbTable extends BaseNoSqlDbTableResource
{
    //*************************************************************************
    //	Constants
    //*************************************************************************

    const TABLE_INDICATOR = 'TableName';

    //*************************************************************************
    //	Members
    //*************************************************************************

    /**
     * @var null|DynamoDb
     */
    protected $parent = null;

    //*************************************************************************
    //	Methods
    //*************************************************************************

    /**
     * @return DynamoDbClient
     */
    protected function getConnection()
    {
        return $this->parent->getConnection();
    }

    /**
     * {@inheritdoc}
     */
    public function retrieveRecordsByFilter($table, $filter = null, $params = [], $extras = [])
    {
        $fields = array_get($extras, ApiOptions::FIELDS);
        $ssFilters = array_get($extras, 'ss_filters');

        $scanProperties = [static::TABLE_INDICATOR => $table];

        $fields = static::buildProjectionExpression($fields);
        if (!empty($fields)) {
            $scanProperties['ProjectionExpression'] = $fields;
        }

        $this->buildFilterExpression($filter, $params, $ssFilters, $scanProperties);

        $limit = intval(array_get($extras, ApiOptions::LIMIT));
        $maxAllowed = $this->getMaxRecordsReturnedLimit();
        if($limit < 1 || $limit > $maxAllowed){
            $limit = $maxAllowed;
        }
        if ($limit > 0) {
            $scanProperties['Limit'] = $limit;
            $scanProperties['Count'] = true;
        }
        $offset = intval(array_get($extras, ApiOptions::OFFSET));
        if ($offset > 0) {
            $scanProperties['ExclusiveStartKey'] = $offset;
            $scanProperties['Count'] = true;
        }

        try {
            $result = $this->getConnection()->scan($scanProperties);
            $items = (array)$result['Items'];

            $out = [];
            foreach ($items as $item) {
                $out[] = $this->unformatAttributes($item);
            }

            $next = $this->unformatAttributes($result['LastEvaluatedKey']);
            $next = current($next); // todo handle more than one index here.
            $count = $result['ScannedCount'];
            $out = static::cleanRecords($out);
            $needMore = ($limit > 0) ? (($count - $offset) > $limit) : false;
            $addCount = array_get_bool($extras, ApiOptions::INCLUDE_COUNT);
            if ($addCount || $needMore || $next) {
                $out['meta']['count'] = $count;
                if ($next) {
                    $out['meta']['next'] = $next;
                }
            }

            return $out;
        } catch (\Exception $ex) {
            throw new InternalServerErrorException("Failed to filter records from '$table'.\n{$ex->getMessage()}");
        }
    }

    /**
     * @param array $record
     *
     * @return mixed
     */
    protected function formatAttributes($record)
    {
        $marshaler = new Marshaler();

        return $marshaler->marshalItem($record);
    }

    /**
     * @param array $native
     *
     * @return array
     */
    protected function unformatAttributes($native)
    {
        $out = [];
        if (is_array($native)) {
            foreach ($native as $key => $value) {
                $out[$key] = static::unformatValue($value);
            }
        }

        return $out;
    }

    protected static function unformatValue($value)
    {
        $marshaler = new Marshaler();

        return $marshaler->unmarshalValue($value);
    }

    protected static function formatValue($value)
    {
        $marshaler = new Marshaler();

        return $marshaler->marshalValue($value);
    }

    protected static function buildProjectionExpression($fields = null, $id_fields = null)
    {
        if (ApiOptions::FIELDS_ALL == $fields) {
            return null;
        }
        if (empty($fields)) {
            if (empty($id_fields)) {
                return null;
            }
            if (is_array($id_fields)) {
                $id_fields = implode(',', $id_fields);
            }

            return $id_fields;
        }

        if (is_array($fields)) {
            $fields = implode(',', $fields);
        }

        return $fields;
    }

    protected function getIdsInfo($table, $fields_info = null, &$requested_fields = null, $requested_types = null)
    {
        $requested_fields = [];
        $result = $this->getConnection()->describeTable([static::TABLE_INDICATOR => $table]);
        $result = $result['Table'];
        $keys = array_get($result, 'KeySchema', []);
        $definitions = array_get($result, 'AttributeDefinitions', []);
        $fields = [];
        foreach ($keys as $key) {
            $name = array_get($key, 'AttributeName');
            $keyType = array_get($key, 'KeyType');
            $type = null;
            foreach ($definitions as $type) {
                if (0 == strcmp($name, array_get($type, 'AttributeName'))) {
                    $type = array_get($type, 'AttributeType');
                }
            }

            $requested_fields[] = $name;
            $fields[] =
                new ColumnSchema(['name' => $name, 'key_type' => $keyType, 'type' => $type, 'required' => true]);
        }

        return $fields;
    }

    protected function checkForIds(&$record, $ids_info, $extras = null, $on_create = false, $remove = false)
    {
        $id = null;
        if (!empty($ids_info)) {
            if (1 == count($ids_info)) {
                $info = $ids_info[0];
                if (is_array($record)) {
                    $value = array_get($record, $info->name);
                    if ($remove) {
                        unset($record[$info->name]);
                    }
                } else {
                    $value = $record;
                }
                if (!empty($value)) {
                    switch ($info->type) {
                        case Type::N:
                            $value = intval($value);
                            break;
                        case Type::S:
                            $value = strval($value);
                            break;
                    }
                    $id = $value;
                } else {
                    // could be passed in as a parameter affecting all records
                    $param = array_get($extras, $info->name);
                    if ($on_create && $info->getRequired() && empty($param)) {
                        return false;
                    }
                }
            } else {
                $id = [];
                foreach ($ids_info as $info) {
                    if (is_array($record)) {
                        $value = array_get($record, $info->name);
                        if ($remove) {
                            unset($record[$info->name]);
                        }
                    } else {
                        $value = $record;
                    }
                    if (!empty($value)) {
                        switch ($info->type) {
                            case Type::N:
                                $value = intval($value);
                                break;
                            case Type::S:
                                $value = strval($value);
                                break;
                        }
                        $id[$info->name] = $value;
                    } else {
                        // could be passed in as a parameter affecting all records
                        $param = array_get($extras, $info->name);
                        if ($on_create && $info->getRequired() && empty($param)) {
                            return false;
                        }
                    }
                }
            }
        }

        if (!empty($id)) {
            return $id;
        } elseif ($on_create) {
            return [];
        }

        return false;
    }

    protected static function buildKey($ids_info, &$record, $remove = false)
    {
        $keys = [];
        foreach ($ids_info as $info) {
            $value = array_get($record, $info->name, null);
            if ($remove) {
                unset($record[$info->name]);
            }
            if (empty($value)) {
                throw new BadRequestException("Identifying field(s) not found in record.");
            }

            switch ($info->type) {
                case Type::N:
                    $value = [Type::N => strval($value)];
                    break;
                default:
                    $value = [Type::S => $value];
            }
            $keys[$info->name] = $value;
        }

        return $keys;
    }

    protected function buildFilterExpression($filter, array $in_params = [], $ss_filters = null, array &$scan = [])
    {
        // interpret any parameter values as lookups
        $params = static::interpretRecordValues($in_params);
        $serverFilter = static::buildSSFilter($ss_filters);

        if (empty($filter)) {
            $filter = $serverFilter;
        } elseif (is_string($filter)) {
            if (!empty($serverFilter)) {
                $filter = '(' . $filter . ') ' . DbLogicalOperators::AND_STR . ' (' . $serverFilter . ')';
            }
        } elseif (is_array($filter)) {
            // todo parse client filter?
            $filter = '';
            if (!empty($serverFilter)) {
                $filter = '(' . $filter . ') ' . DbLogicalOperators::AND_STR . ' (' . $serverFilter . ')';
            }
        }

        Session::replaceLookups($filter);
        $outNames = [];
        $outParams = [];
        if (!empty($parsed = $this->parseFilterString($filter, $outNames, $outParams, $params))) {
            $scan['FilterExpression'] = $parsed;
            if (!empty($outNames)) {
                $scan['ExpressionAttributeNames'] = $outNames;
            }
            if (!empty($outParams)) {
                $scan['ExpressionAttributeValues'] = $outParams;
            }
        }
    }

    protected static function buildSSFilter($ss_filters)
    {
        if (empty($ss_filters)) {
            return '';
        }

        // build the server side criteria
        $filters = array_get($ss_filters, 'filters');
        if (empty($filters)) {
            return '';
        }

        $sql = '';
        $combiner = array_get($ss_filters, 'filter_op', DbLogicalOperators::AND_STR);
        switch (strtoupper($combiner)) {
            case 'AND':
            case 'OR':
                break;
            default:
                // log and bail
                throw new InternalServerErrorException('Invalid server-side filter configuration detected.');
        }
        foreach ($filters as $key => $filter) {
            if (!empty($sql)) {
                $sql .= " $combiner ";
            }

            $name = array_get($filter, 'name');
            $op = strtoupper(array_get($filter, 'operator'));
            if (empty($name) || empty($op)) {
                // log and bail
                throw new InternalServerErrorException('Invalid server-side filter configuration detected.');
            }

            if (DbComparisonOperators::requiresNoValue($op)) {
                $sql .= "($name $op)";
            } else {
                $value = array_get($filter, 'value');
                $sql .= "($name $op $value)";
            }
        }

        return $sql;
    }

    public static function localizeOperator($operator)
    {
        switch ($operator) {
            case DbComparisonOperators::NE_STR:
            case DbComparisonOperators::NE:
                return DbComparisonOperators::NE_2;
            // Value-Modifying Operators
            case DbComparisonOperators::CONTAINS:
            case DbComparisonOperators::STARTS_WITH:
            case DbComparisonOperators::ENDS_WITH:
                return DbComparisonOperators::LIKE;
            default:
                return parent::localizeOperator($operator);
        }
    }

    /**
     * @param string $filter
     * @param array  $out_names
     * @param array  $out_params
     * @param array  $in_params
     *
     * @return string
     * @throws \DreamFactory\Core\Exceptions\BadRequestException
     * @throws \Exception
     */
    protected function parseFilterString($filter, array &$out_names, array &$out_params, array $in_params = [])
    {
        if (empty($filter)) {
            return null;
        }

        $filter = trim($filter);
        // todo use smarter regex
        // handle logical operators first
        $logicalOperators = DbLogicalOperators::getDefinedConstants();
        foreach ($logicalOperators as $logicalOp) {
            if (DbLogicalOperators::NOT_STR === $logicalOp) {
                // NOT(a = 1)  or NOT (a = 1)format
                if ((0 === stripos($filter, $logicalOp . ' (')) || (0 === stripos($filter, $logicalOp . '('))) {
                    $parts = trim(substr($filter, 3));
                    $parts = $this->parseFilterString($parts, $out_names, $out_params, $in_params);

                    return static::localizeOperator($logicalOp) . $parts;
                }
            } else {
                // (a = 1) AND (b = 2) format or (a = 1)AND(b = 2) format
                $filter = str_ireplace(')' . $logicalOp . '(', ') ' . $logicalOp . ' (', $filter);
                $paddedOp = ') ' . $logicalOp . ' (';
                if (false !== $pos = stripos($filter, $paddedOp)) {
                    $left = trim(substr($filter, 0, $pos)) . ')'; // add back right )
                    $right = '(' . trim(substr($filter, $pos + strlen($paddedOp))); // adding back left (
                    $left = $this->parseFilterString($left, $out_names, $out_params, $in_params);
                    $right = $this->parseFilterString($right, $out_names, $out_params, $in_params);

                    return $left . ' ' . static::localizeOperator($logicalOp) . ' ' . $right;
                }
            }
        }

        $wrap = false;
        if ((0 === strpos($filter, '(')) && ((strlen($filter) - 1) === strrpos($filter, ')'))) {
            // remove unnecessary wrapping ()
            $filter = substr($filter, 1, -1);
            $wrap = true;
        }

        // Some scenarios leave extra parens dangling
        $pure = trim($filter, '()');
        $pieces = explode($pure, $filter);
        $leftParen = (!empty($pieces[0]) ? $pieces[0] : null);
        $rightParen = (!empty($pieces[1]) ? $pieces[1] : null);
        $filter = $pure;

        // the rest should be comparison operators
        // Note: order matters here!
        $sqlOperators = DbComparisonOperators::getParsingOrder();
        foreach ($sqlOperators as $sqlOp) {
            $paddedOp = static::padOperator($sqlOp);
            if (false !== $pos = stripos($filter, $paddedOp)) {
                $field = trim(substr($filter, 0, $pos));
                $negate = false;
                if (false !== strpos($field, ' ')) {
                    $parts = explode(' ', $field);
                    $partsCount = count($parts);
                    if (($partsCount > 1) &&
                        (0 === strcasecmp($parts[$partsCount - 1], trim(DbLogicalOperators::NOT_STR)))
                    ) {
                        // negation on left side of operator
                        array_pop($parts);
                        $field = implode(' ', $parts);
                        $negate = true;
                    }
                }
                /** @type ColumnSchema $info */
                if (null === $info = new ColumnSchema(['name' => strtolower($field)])) {
                    // This could be SQL injection attempt or bad field
                    throw new BadRequestException("Invalid or unparsable field in filter request: '$field'");
                }

                // make sure we haven't chopped off right side too much
                $value = trim(substr($filter, $pos + strlen($paddedOp)));
                if ((0 !== strpos($value, "'")) &&
                    (0 !== $lpc = substr_count($value, '(')) &&
                    ($lpc !== $rpc = substr_count($value, ')'))
                ) {
                    // add back to value from right
                    $parenPad = str_repeat(')', $lpc - $rpc);
                    $value .= $parenPad;
                    $rightParen = preg_replace('/\)/', '', $rightParen, $lpc - $rpc);
                }
                if (DbComparisonOperators::requiresValueList($sqlOp)) {
                    if ((0 === strpos($value, '(')) && ((strlen($value) - 1) === strrpos($value, ')'))) {
                        // remove wrapping ()
                        $value = substr($value, 1, -1);
                        $parsed = [];
                        foreach (explode(',', $value) as $each) {
                            $parsed[] = $this->parseFilterValue(trim($each), $info, $out_params, $in_params);
                        }
                        $value = '(' . implode(',', $parsed) . ')';
                    } else {
                        throw new BadRequestException('Filter value lists must be wrapped in parentheses.');
                    }
                } elseif (DbComparisonOperators::requiresNoValue($sqlOp)) {
                    switch ($sqlOp) {
                        case DbComparisonOperators::IS_NULL:
                            $value = null;
                            $sqlOp = DbComparisonOperators::EQ;
                            break;
                        case DbComparisonOperators::IS_NOT_NULL:
                            $value = null;
                            $sqlOp = DbComparisonOperators::NE_2;
                            break;
                        case DbComparisonOperators::DOES_EXIST:
                            $out_names['#' . $field] = $field;

                            return "attribute_exists(#$field)";
                        case DbComparisonOperators::DOES_NOT_EXIST:
                            $out_names['#' . $field] = $field;

                            return "attribute_not_exists(#$field)";
                    }
                } else {
                    static::modifyValueByOperator($sqlOp, $value);
                    switch ($sqlOp) {
                        case DbComparisonOperators::LIKE:
                        case DbComparisonOperators::CONTAINS:
                        case DbComparisonOperators::STARTS_WITH:
                        case DbComparisonOperators::ENDS_WITH:
                            $out_names['#' . $field] = $field;
                            if ('%' == $value[strlen($value) - 1]) {
                                if ('%' == $value[0]) {
                                    $value = trim($value, '%');
                                    $value = $this->parseFilterValue($value, $info, $out_params, $in_params);

                                    return "contains(#$field, $value)";
                                } else {
                                    $value = trim($value, '%');
                                    $value = $this->parseFilterValue($value, $info, $out_params, $in_params);

                                    return "begins_with(#$field, $value)";
                                }
                            } elseif ('%' == $value[0]) {
                                throw new BadRequestException('ENDS WITH operator not supported on this service.');
                            }

                            $value = $this->parseFilterValue($value, $info, $out_params, $in_params);

                            return "contains(#$field, $value)";
                    }
                    $value = $this->parseFilterValue($value, $info, $out_params, $in_params);
                }

                $sqlOp = static::localizeOperator($sqlOp);
                if ($negate) {
                    $sqlOp = DbLogicalOperators::NOT_STR . ' ' . $sqlOp;
                }

                $out_names['#' . $field] = $field;
                $out = '#' . $field . ' ' . $sqlOp;
                $out .= (isset($value) ? " $value" : null);
                if ($leftParen) {
                    $out = $leftParen . $out;
                }
                if ($rightParen) {
                    $out .= $rightParen;
                }

                return ($wrap ? '(' . $out . ')' : $out);
            }
        }

        // This could be SQL injection attempt or unsupported filter arrangement
        throw new BadRequestException('Invalid or unparsable filter request.');
    }

    /**
     * @param mixed        $value
     * @param ColumnSchema $info
     * @param array        $out_params
     * @param array        $in_params
     *
     * @return int|null|string
     * @throws BadRequestException
     */
    protected function parseFilterValue($value, ColumnSchema $info, array &$out_params, array $in_params = [])
    {
        // if a named replacement parameter, un-name it because Laravel can't handle named parameters
        if (is_array($in_params) && (0 === strpos($value, ':'))) {
            if (array_key_exists($value, $in_params)) {
                $value = $in_params[$value];
            }
        }

        // remove quoting on strings if used, i.e. 1.x required them
        if (is_string($value)) {
            if ((0 === strcmp("'" . trim($value, "'") . "'", $value)) ||
                (0 === strcmp('"' . trim($value, '"') . '"', $value))
            ) {
                $value = substr($value, 1, -1);
            }
        }

        if (trim($value, "'\"") !== $value) {
            $value = trim($value, "'\""); // meant to be a string
        } elseif (is_numeric($value)) {
            $value = ($value == strval(intval($value))) ? intval($value) : floatval($value);
        } elseif (0 == strcasecmp($value, 'true')) {
            $value = true;
        } elseif (0 == strcasecmp($value, 'false')) {
            $value = false;
        }

        $out_params[':' . $info->name] = static::formatValue($value);
        $value = ':' . $info->name;

        return $value;
    }

    /**
     * @inheritdoc
     */
    protected function parseValueForSet($value, $field_info, $for_update = false)
    {
        if (DbSimpleTypes::TYPE_ID === $field_info->type) {
            // may be hash string or int, don't convert to int automatically
            return $value;
        } else {
            return parent::parseValueForSet($value, $field_info, $for_update);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function addToTransaction(
        $record = null,
        $id = null,
        $extras = null,
        $rollback = false,
        $continue = false,
        $single = false
    ) {
        $ssFilters = array_get($extras, 'ss_filters');
        $fields = array_get($extras, ApiOptions::FIELDS);
        $idFields = array_get($extras, 'id_fields');
        $updates = array_get($extras, 'updates');

        $out = [];
        switch ($this->getAction()) {
            case Verbs::POST:
                $parsed = $this->parseRecord($record, $this->tableFieldsInfo, $ssFilters);
                if (empty($parsed)) {
                    throw new BadRequestException('No valid fields were found in record.');
                }

                $item = [
                    static::TABLE_INDICATOR => $this->transactionTable,
                    'Item'                  => $this->formatAttributes($parsed),
                    'ConditionExpression'   => "attribute_not_exists($idFields[0])",
                ];

                $this->getConnection()->putItem($item);

                if ($rollback) {
                    $key = static::buildKey($this->tableIdsInfo, $record);
                    $this->addToRollback($key);
                }

                $out = static::cleanRecord($record, $fields, $idFields);
                break;
            case Verbs::PUT:
                if (!empty($updates)) {
                    // only update by full records can use batching
                    $updates[$idFields[0]] = $id;
                    $record = $updates;
                }

                $parsed = $this->parseRecord($record, $this->tableFieldsInfo, $ssFilters, true);
                if (empty($parsed)) {
                    throw new BadRequestException('No valid fields were found in record.');
                }

                $native = $this->formatAttributes($parsed);

                $upsert = $this->parent->upsertAllowed();
                if (!$continue && !$rollback && !$upsert) {
                    return parent::addToTransaction($native, $id);
                }

                $options = ($rollback) ? ReturnValue::ALL_OLD : ReturnValue::NONE;
                $item = [
                    static::TABLE_INDICATOR => $this->transactionTable,
                    'Item'                  => $native,
                    'ReturnValues'          => $options,
                ];

                if (!$upsert) {
                    $item['ConditionExpression'] = "attribute_exists($idFields[0])";
                }

                $result = $this->getConnection()->putItem($item);

                if ($rollback) {
                    $temp = $result['Attributes'];
                    if (!empty($temp)) {
                        $this->addToRollback($temp);
                    }
                }

                $out = static::cleanRecord($record, $fields, $idFields);
                break;

            case Verbs::PATCH:
                if (!empty($updates)) {
                    $updates[$idFields[0]] = $id;
                    $record = $updates;
                }

                $parsed = $this->parseRecord($record, $this->tableFieldsInfo, $ssFilters, true);
                if (empty($parsed)) {
                    throw new BadRequestException('No valid fields were found in record.');
                }

                $key = static::buildKey($this->tableIdsInfo, $parsed, true);
                $marshaler = new Marshaler();
                $expression = [];
                $outNames = [];
                $outParams = [];
                foreach ($parsed as $name => &$value) {
                    $outNames['#' . $name] = $name;
                    $outParams[':' . $name] = $marshaler->marshalValue($value);
                    $expression[] = "#$name = :$name";
                }

                $expression = (!empty($expression) ? 'SET ' . implode(', ', $expression) : null);

                // simple insert request
                $options = ($rollback) ? ReturnValue::ALL_OLD : ReturnValue::ALL_NEW;

                $result = $this->getConnection()->updateItem(
                    [
                        static::TABLE_INDICATOR     => $this->transactionTable,
                        'Key'                       => $key,
                        'UpdateExpression'          => $expression,
                        'ExpressionAttributeNames'  => $outNames,
                        'ExpressionAttributeValues' => $outParams,
                        'ReturnValues'              => $options
                    ]
                );

                $temp = $result['Attributes'];
                if ($rollback) {
                    $this->addToRollback($temp);

                    // merge old record with new changes
                    $new = array_merge($this->unformatAttributes($temp), $updates);
                    $out = static::cleanRecord($new, $fields, $idFields);
                } else {
                    $temp = $this->unformatAttributes($temp);
                    $out = static::cleanRecord($temp, $fields, $idFields);
                }
                break;

            case Verbs::DELETE:
                if (!$continue && !$rollback) {
                    return parent::addToTransaction(null, $id);
                }

                $record = [$idFields[0] => $id];
                $key = static::buildKey($this->tableIdsInfo, $record);

                $result = $this->getConnection()->deleteItem(
                    [
                        static::TABLE_INDICATOR => $this->transactionTable,
                        'Key'                   => $key,
                        'ReturnValues'          => ReturnValue::ALL_OLD,
                    ]
                );

                $temp = $result['Attributes'];

                if ($rollback) {
                    $this->addToRollback($temp);
                }

                $temp = $this->unformatAttributes($temp);
                $out = static::cleanRecord($temp, $fields, $idFields);
                break;

            case Verbs::GET:
                $record = [$idFields[0] => $id];
                $key = static::buildKey($this->tableIdsInfo, $record);
                $scanProperties = [
                    static::TABLE_INDICATOR => $this->transactionTable,
                    'Key'                   => $key,
                    'ConsistentRead'        => true,
                ];

                $fields = static::buildProjectionExpression($fields, $idFields);
                if (!empty($fields)) {
                    $scanProperties['ProjectionExpression'] = $fields;
                }

                $result = $this->getConnection()->getItem($scanProperties);
                $result = $result['Item'];
                if (empty($result)) {
                    throw new NotFoundException('Record not found.');
                }

                // Grab value from the result object like an array
                $out = $this->unformatAttributes($result);
                break;
            default:
                break;
        }

        return $out;
    }

    /**
     * {@inheritdoc}
     */
    protected function commitTransaction($extras = null)
    {
        if (empty($this->batchRecords) && empty($this->batchIds)) {
            return null;
        }

//        $ssFilters = array_get( $extras, 'ss_filters' );
        $fields = array_get($extras, ApiOptions::FIELDS);
        $requireMore = array_get($extras, 'require_more');
        $idFields = array_get($extras, 'id_fields');

        $out = [];
        switch ($this->getAction()) {
            case Verbs::POST:
                $requests = [];
                foreach ($this->batchRecords as $item) {
                    $requests[] = ['PutRequest' => ['Item' => $item]];
                }

                /*$result = */
                $this->getConnection()->batchWriteItem(
                    ['RequestItems' => [$this->transactionTable => $requests]]
                );

                // todo check $result['UnprocessedItems'] for 'PutRequest'

                foreach ($this->batchRecords as $item) {
                    $out[] = static::cleanRecord($this->unformatAttributes($item), $fields, $idFields);
                }
                break;

            case Verbs::PUT:
                $requests = [];
                foreach ($this->batchRecords as $item) {
                    $requests[] = ['PutRequest' => ['Item' => $item]];
                }

                /*$result = */
                $this->getConnection()->batchWriteItem(
                    ['RequestItems' => [$this->transactionTable => $requests]]
                );

                // todo check $result['UnprocessedItems'] for 'PutRequest'

                foreach ($this->batchRecords as $item) {
                    $out[] = static::cleanRecord($this->unformatAttributes($item), $fields, $idFields);
                }
                break;

            case Verbs::PATCH:
                throw new BadRequestException('Batch operation not supported for patch.');
                break;

            case Verbs::DELETE:
                $requests = [];
                foreach ($this->batchIds as $id) {
                    $record = [$idFields[0] => $id];
                    $out[] = $record;
                    $key = static::buildKey($this->tableIdsInfo, $record);
                    $requests[] = ['DeleteRequest' => ['Key' => $key]];
                }
                if ($requireMore) {
                    $scanProperties = [
                        'Keys'           => $this->batchRecords,
                        'ConsistentRead' => true,
                    ];

                    $attributes = static::buildProjectionExpression($fields, $idFields);
                    if (!empty($attributes)) {
                        $scanProperties['ProjectionExpression'] = $attributes;
                    }

                    // Get multiple items by key in a BatchGetItem request
                    $result = $this->getConnection()->batchGetItem(
                        [
                            'RequestItems' => [
                                $this->transactionTable => $scanProperties
                            ]
                        ]
                    );

                    $out = [];
                    $items = $result->search(str_replace('/', '.', "Responses/{$this->transactionTable}"));
                    foreach ($items as $item) {
                        $out[] = $this->unformatAttributes($item);
                    }
                }

                /*$result = */
                $this->getConnection()->batchWriteItem(
                    ['RequestItems' => [$this->transactionTable => $requests]]
                );

                // todo check $result['UnprocessedItems'] for 'DeleteRequest'
                break;

            case Verbs::GET:
                $keys = [];
                foreach ($this->batchIds as $id) {
                    $record = [$idFields[0] => $id];
                    $key = static::buildKey($this->tableIdsInfo, $record);
                    $keys[] = $key;
                }

                $scanProperties = [
                    'Keys'           => $keys,
                    'ConsistentRead' => true,
                ];

                $fields = static::buildProjectionExpression($fields, $idFields);
                if (!empty($fields)) {
                    $scanProperties['ProjectionExpression'] = $fields;
                }

                // Get multiple items by key in a BatchGetItem request
                $result = $this->getConnection()->batchGetItem(
                    [
                        'RequestItems' => [
                            $this->transactionTable => $scanProperties
                        ]
                    ]
                );

                $items = $result->search(str_replace('/', '.', "Responses/{$this->transactionTable}"));
                foreach ($items as $item) {
                    $out[] = $this->unformatAttributes($item);
                }
                break;
            default:
                break;
        }

        $this->batchIds = [];
        $this->batchRecords = [];

        return $out;
    }

    /**
     * {@inheritdoc}
     */
    protected function addToRollback($record)
    {
        return parent::addToRollback($record);
    }

    /**
     * {@inheritdoc}
     */
    protected function rollbackTransaction()
    {
        if (!empty($this->rollbackRecords)) {
            switch ($this->getAction()) {
                case Verbs::POST:
                    $requests = [];
                    foreach ($this->rollbackRecords as $item) {
                        $requests[] = ['DeleteRequest' => ['Key' => $item]];
                    }

                    /* $result = */
                    $this->getConnection()->batchWriteItem(
                        ['RequestItems' => [$this->transactionTable => $requests]]
                    );

                    // todo check $result['UnprocessedItems'] for 'DeleteRequest'
                    break;

                case Verbs::PUT:
                case Verbs::PATCH:
                case Verbs::DELETE:
                    $requests = [];
                    foreach ($this->rollbackRecords as $item) {
                        $requests[] = ['PutRequest' => ['Item' => $item]];
                    }

                    /* $result = */
                    $this->getConnection()->batchWriteItem(
                        ['RequestItems' => [$this->transactionTable => $requests]]
                    );

                    // todo check $result['UnprocessedItems'] for 'PutRequest'
                    break;

                default:
                    break;
            }

            $this->rollbackRecords = [];
        }

        return true;
    }
}


<?php
namespace DreamFactory\Core\Aws\Resources;

use DreamFactory\Core\Aws\Services\Sns;
use DreamFactory\Core\Exceptions\BadRequestException;
use DreamFactory\Core\Exceptions\InternalServerErrorException;
use DreamFactory\Core\Exceptions\NotFoundException;
use DreamFactory\Core\Contracts\ServiceResponseInterface;

/**
 * Class SnsApplication
 *
 * @package DreamFactory\Core\Aws\Resources
 */
class SnsApplication extends BaseSnsResource
{
    //*************************************************************************
    //	Constants
    //*************************************************************************

    /**
     * Resource tag for dealing with application
     */
    const RESOURCE_NAME = 'app';

    //*************************************************************************
    //	Members
    //*************************************************************************

    //*************************************************************************
    //	Methods
    //*************************************************************************

    /**
     * {@inheritdoc}
     */
    protected static function getResourceIdentifier()
    {
        return 'PlatformApplicationArn';
    }

    /**
     * {@inheritdoc}
     */
    public function getResources()
    {
//        $refresh = $this->request->queryBool('refresh');

        $out = [];
        $token = null;
        try {
            do {
                $result = $this->service->getConnection()->listPlatformApplications(
                    [
                        'NextToken' => $token
                    ]
                );
                $topics = $result['PlatformApplications'];
                $token = $result['NextToken'];

                if (!empty($topics)) {
                    $out = array_merge($out, $topics);
                }
            } while ($token);
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to retrieve applications.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return $out;
    }

    /**
     * Apply the commonly used REST path members to the class.
     *
     * @param string $resourcePath
     *
     * @return $this
     */
    protected function setResourceMembers($resourcePath = null)
    {
        parent::setResourceMembers($resourcePath);

        $this->resource = array_get($this->resourceArray, 0);

        $pos = 1;
        $more = array_get($this->resourceArray, $pos);

        if (!empty($more)) {
            if (SnsEndpoint::RESOURCE_NAME !== $more) {
                do {
                    $this->resource .= '/' . $more;
                    $pos++;
                    $more = array_get($this->resourceArray, $pos);
                } while (!empty($more) && (SnsEndpoint::RESOURCE_NAME !== $more));
            }
        }

        return $this;
    }

    /**
     * @param mixed $fields Use boolean, comma-delimited string, or array of properties
     *
     * @return ServiceResponseInterface
     */
    public function listResources($fields = null)
    {
        $resources = [];
        $result = $this->getResources();
        foreach ($result as $app) {
            switch ($fields) {
                case false:
                case Sns::FORMAT_SIMPLE:
                    $resources[] = $this->service->stripArnPrefix(array_get($app, 'PlatformApplicationArn'));
                    break;
                case Sns::FORMAT_ARN:
                    $resources[] = array_get($app, 'PlatformApplicationArn');
                    break;
                case true:
                case Sns::FORMAT_FULL:
                default:
                    $app['Application'] =
                        $this->service->stripArnPrefix(array_get($app, 'PlatformApplicationArn'));
                    $resources[] = $app;
                    break;
            }
        }

        return $resources;
    }

    protected function handleGET()
    {
        if (empty($this->resource)) {
            return parent::handleGET();
        } else {
            return $this->retrieveApplication($this->resource);
        }
    }

    protected function handlePOST()
    {
        $payload = $this->request->getPayloadData();
        if (empty($payload)) {
            throw new BadRequestException('No data in application post request.');
        }

        if (empty($this->resource)) {
            return $this->createApplication($payload);
        } else {
            return false;
        }
    }

    protected function handlePUT()
    {
        $payload = $this->request->getPayloadData();
        if (empty($payload)) {
            throw new BadRequestException('No data in application update request.');
        }

        if (!empty($this->resource)) {
            $payload['Topic'] = $this->resource;
        }

        return $this->updateApplication($payload);
    }

    protected function handlePATCH()
    {
        return $this->handlePUT();
    }

    protected function handleDELETE()
    {
        $payload = $this->request->getPayloadData();
        if (empty($this->resource)) {
            if (empty($payload)) {
                throw new BadRequestException('No data in application delete request.');
            }

            $this->deleteApplication($payload);
        } else {
            $this->deleteApplication($this->resource);
        }

        return ['success' => true];
    }

    /**
     * @param $resource
     *
     * @return array
     * @throws InternalServerErrorException
     * @throws NotFoundException
     */
    public function retrieveApplication($resource)
    {
        $request = ['PlatformApplicationArn' => $this->service->addArnPrefix($resource)];

        try {
            if (null !== $result = $this->service->getConnection()->getPlatformApplicationAttributes($request)) {
                $attributes = array_get($result->toArray(), 'Attributes');

                return [
                    'Application'            => $this->service->stripArnPrefix($resource),
                    'PlatformApplicationArn' => $this->service->addArnPrefix($resource),
                    'Attributes'             => $attributes
                ];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to retrieve properties for '$resource'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }

    public function createApplication($request)
    {
        if (is_array($request)) {
            $name = array_get($request, 'Name');
            if (empty($name)) {
                throw new BadRequestException("Create application request contains no 'Name' field.");
            }
        } else {
            throw new BadRequestException("Create application request contains no fields.");
        }

        try {
            if (null !== $result = $this->service->getConnection()->createPlatformApplication($request)) {
                $arn = array_get($result->toArray(), 'PlatformApplicationArn', '');

                return ['Application' => $this->service->stripArnPrefix($arn), 'PlatformApplicationArn' => $arn];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to create application '{$request['Name']}'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }

    public function updateApplication($request)
    {
        if (is_array($request)) {
            $name = array_get($request, 'Application', array_get($request, 'PlatformApplicationArn'));
            if (empty($name)) {
                throw new BadRequestException("Update application request contains no 'Application' field.");
            }

            $request['PlatformApplicationArn'] = $this->service->addArnPrefix($name);
        } else {
            throw new BadRequestException("Update application request contains no fields.");
        }

        try {
            if (null !== $result = $this->service->getConnection()->setPlatformApplicationAttributes($request)) {
                return ['success' => true];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException(
                "Failed to update application '{$request['PlatformApplicationArn']}'.\n{$ex->getMessage()}",
                $ex->getCode()
            );
        }

        return [];
    }

    public function deleteApplication($request)
    {
        $data = [];
        if (is_array($request)) {
            $name = array_get($request, 'Application', array_get($request, 'PlatformApplicationArn'));
            if (empty($name)) {
                throw new BadRequestException("Delete application request contains no 'Application' field.");
            }

            $data['PlatformApplicationArn'] = $this->service->addArnPrefix($name);
        } else {
            $data['PlatformApplicationArn'] = $this->service->addArnPrefix($request);
        }

        try {
            if (null !== $result = $this->service->getConnection()->deletePlatformApplication($data)) {
                return ['success' => true];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException(
                "Failed to delete application '{$data['PlatformApplicationArn']}'.\n{$ex->getMessage()}",
                $ex->getCode()
            );
        }

        return [];
    }
}


<?php
namespace DreamFactory\Core\Aws\Resources;

use DreamFactory\Core\Aws\Services\Sns;
use DreamFactory\Core\Exceptions\BadRequestException;
use DreamFactory\Core\Exceptions\InternalServerErrorException;
use DreamFactory\Core\Exceptions\NotFoundException;
use DreamFactory\Core\Contracts\ServiceResponseInterface;

/**
 * Class SnsTopic
 *
 * @package DreamFactory\Core\Aws\Resources
 */
class SnsTopic extends BaseSnsResource
{
    //*************************************************************************
    //	Constants
    //*************************************************************************

    /**
     * Resource tag for dealing with topics
     */
    const RESOURCE_NAME = 'topic';

    //*************************************************************************
    //	Members
    //*************************************************************************

    //*************************************************************************
    //	Methods
    //*************************************************************************

    /**
     * {@inheritdoc}
     */
    protected static function getResourceIdentifier()
    {
        return 'TopicArn';
    }

    /**
     * {@inheritdoc}
     */
    public function getResources()
    {
//        $refresh = $this->request->queryBool('refresh');

        $out = [];
        $token = null;
        try {
            do {
                $result = $this->service->getConnection()->listTopics(
                    [
                        'NextToken' => $token
                    ]
                );
                $topics = $result['Topics'];
                $token = $result['NextToken'];

                if (!empty($topics)) {
                    $out = array_merge($out, $topics);
                }
            } while ($token);
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to retrieve topics.\n{$ex->getMessage()}", $ex->getCode());
        }

        return $out;
    }

    /**
     * @param mixed $fields Use boolean, comma-delimited string, or array of properties
     *
     * @return ServiceResponseInterface
     */
    public function listResources($fields = null)
    {
        $resources = [];
        $result = $this->getResources();
        foreach ($result as $topic) {
            switch ($fields) {
                case false:
                case Sns::FORMAT_SIMPLE:
                    $resources[] = $this->service->stripArnPrefix(array_get($topic, 'TopicArn'));
                    break;
                case Sns::FORMAT_ARN:
                    $resources[] = array_get($topic, 'TopicArn');
                    break;
                case true:
                case Sns::FORMAT_FULL:
                default:
                    $topic['Topic'] = $this->service->stripArnPrefix(array_get($topic, 'TopicArn'));
                    $resources[] = $topic;
                    break;
            }
        }

        return $resources;
    }

    protected function handleGET()
    {
        if (empty($this->resource)) {
            return parent::handleGET();
        } else {
            return $this->retrieveTopic($this->resource);
        }
    }

    protected function handlePOST()
    {
        $payload = $this->request->getPayloadData();
        if (empty($payload)) {
            throw new BadRequestException('No data in topic post request.');
        }

        if (empty($this->resource)) {
            return $this->createTopic($payload);
        } else {
            return $this->service->publish($payload, static::RESOURCE_NAME, $this->resource);
        }
    }

    protected function handlePUT()
    {
        $payload = $this->request->getPayloadData();
        if (empty($payload)) {
            throw new BadRequestException('No data in topic update request.');
        }

        if (!empty($this->resource)) {
            $payload['Topic'] = $this->resource;
        }

        return $this->updateTopic($payload);
    }

    protected function handlePATCH()
    {
        return $this->handlePUT();
    }

    protected function handleDELETE()
    {
        $payload = $this->request->getPayloadData();
        if (empty($this->resource)) {
            if (empty($payload)) {
                throw new BadRequestException('No data in topic delete request.');
            }

            $this->deleteTopic($payload);
        } else {
            $this->deleteTopic($this->resource);
        }

        return ['success' => true];
    }

    /**
     * @param $resource
     *
     * @return array
     * @throws InternalServerErrorException
     * @throws NotFoundException
     */
    public function retrieveTopic($resource)
    {
        $request = ['TopicArn' => $this->service->addArnPrefix($resource)];

        try {
            if (null !== $result = $this->service->getConnection()->getTopicAttributes($request)) {
                $out = array_get($result->toArray(), 'Attributes');
                $out['Topic'] = $this->service->stripArnPrefix($resource);

                return $out;
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to retrieve properties for '$resource'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }

    public function createTopic($request)
    {
        $data = [];
        if (is_array($request)) {
            $name = array_get($request, 'Name');
            if (empty($name)) {
                throw new BadRequestException("Create Topic request contains no 'Name' field.");
            }

            $data['Name'] = $name;
        } else {
            $data['Name'] = $request;
        }

        try {
            if (null !== $result = $this->service->getConnection()->createTopic($data)) {
                $arn = array_get($result->toArray(), 'TopicArn', '');

                return ['Topic' => $this->service->stripArnPrefix($arn), 'TopicArn' => $arn];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to create topic '{$data['Name']}'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }

    public function updateTopic($request)
    {
        if (is_array($request)) {
            $name = array_get($request, 'Topic', array_get($request, 'TopicArn'));
            if (empty($name)) {
                throw new BadRequestException("Update topic request contains no 'Topic' field.");
            }

            $request['TopicArn'] = $this->service->addArnPrefix($name);
        } else {
            throw new BadRequestException("Update topic request contains no fields.");
        }

        try {
            if (null !== $result = $this->service->getConnection()->setTopicAttributes($request)) {
                return ['success' => true];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to update topic '{$request['TopicArn']}'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }

    public function deleteTopic($request)
    {
        $data = [];
        if (is_array($request)) {
            $name = array_get($request, 'Topic', array_get($request, 'TopicArn'));
            if (empty($name)) {
                throw new BadRequestException("Delete Topic request contains no 'Topic' field.");
            }

            $data['TopicArn'] = $this->service->addArnPrefix($name);
        } else {
            $data['TopicArn'] = $this->service->addArnPrefix($request);
        }

        try {
            if (null !== $result = $this->service->getConnection()->deleteTopic($data)) {
                return ['success' => true];
            }
        } catch (\Exception $ex) {
            if (null !== $newEx = Sns::translateException($ex)) {
                throw $newEx;
            }

            throw new InternalServerErrorException("Failed to delete topic '{$data['TopicArn']}'.\n{$ex->getMessage()}",
                $ex->getCode());
        }

        return [];
    }
}