Add Query Converter

This commit is contained in:
Bradley Schofield 2024-02-08 11:39:01 +00:00
parent 70ea6734e7
commit 38c0f8ddab
3 changed files with 376 additions and 29 deletions

47
composer.lock generated
View file

@ -1029,16 +1029,16 @@
},
{
"name": "symfony/polyfill-php80",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"shasum": ""
},
"require": {
@ -1046,9 +1046,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -1092,7 +1089,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
},
"funding": [
{
@ -1108,7 +1105,7 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "thecodingmachine/safe",
@ -5123,16 +5120,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
"shasum": ""
},
"require": {
@ -5146,9 +5143,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -5185,7 +5179,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
},
"funding": [
{
@ -5201,20 +5195,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "42292d99c55abe617799667f454222c54c60e229"
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "42292d99c55abe617799667f454222c54c60e229",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"shasum": ""
},
"require": {
@ -5228,9 +5222,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@ -5268,7 +5259,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
},
"funding": [
{
@ -5284,7 +5275,7 @@
"type": "tidelift"
}
],
"time": "2023-07-28T09:04:16+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "textalk/websocket",
@ -5523,5 +5514,5 @@
"platform-overrides": {
"php": "8.2"
},
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.6.0"
}

View file

@ -3,9 +3,20 @@
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Utopia\Request\Filter;
use Utopia\Database\Query;
class V17 extends Filter
{
protected const CHAR_SINGLE_QUOTE = '\'';
protected const CHAR_DOUBLE_QUOTE = '"';
protected const CHAR_COMMA = ',';
protected const CHAR_SPACE = ' ';
protected const CHAR_BRACKET_START = '[';
protected const CHAR_BRACKET_END = ']';
protected const CHAR_PARENTHESES_START = '(';
protected const CHAR_PARENTHESES_END = ')';
protected const CHAR_BACKSLASH = '\\';
// Convert 1.4 params to 1.5
public function parse(array $content, string $model): array
{
@ -13,7 +24,319 @@ class V17 extends Filter
case 'account.updateRecovery':
unset($content['passwordAgain']);
break;
// Queries
case 'account.listIdentities':
case 'account.listLogs':
case 'databases.list':
case 'databases.listLogs':
case 'databases.listCollections':
case 'databases.listCollectionLogs':
case 'databases.listAttributes':
case 'databases.listIndexes':
case 'databases.listDocuments':
case 'databases.getDocument':
case 'databases.listDocumentLogs':
case 'functions.list':
case 'functions.listDeployments':
case 'functions.listExecutions':
case 'migrations.list':
case 'projects.list':
case 'proxy.listRules':
case 'storage.listBuckets':
case 'storage.listFiles':
case 'teams.list':
case 'teams.listMemberships':
case 'teams.listLogs':
case 'users.list':
case 'users.listLogs':
case 'users.listIdentities':
case 'vcs.listInstallations':
$content = $this->convertOldQueries($content);
break;
}
return $content;
}
function convertOldQueries(array $content): array
{
$parsed = [];
foreach ($content['queries'] as $query) {
try {
$query = $this->parseQuery($query);
$parsed[] = json_encode(array_filter($query->toArray()));
} catch (\Throwable $th) {
throw new \Exception("Invalid query: {$query}", previous: $th);
}
}
$content['queries'] = $parsed;
return $content;
}
// 1.4 query parser
public function parseQuery(string $filter): Query
{
// Init empty vars we fill later
$method = '';
$params = [];
// Separate method from filter
$paramsStart = mb_strpos($filter, '(');
if ($paramsStart === false) {
throw new \Exception('Invalid query');
}
$method = mb_substr($filter, 0, $paramsStart);
// Separate params from filter
$paramsEnd = \strlen($filter) - 1; // -1 to ignore )
$parametersStart = $paramsStart + 1; // +1 to ignore (
// Check for deprecated query syntax
if (\str_contains($method, '.')) {
throw new \Exception('Invalid query method');
}
$currentParam = ""; // We build param here before pushing when it's ended
$currentArrayParam = []; // We build array param here before pushing when it's ended
$stack = []; // State for stack of parentheses
$stackCount = 0; // Length of stack array. Kept as variable to improve performance
$stringStackState = null; // State for string support
// Loop thorough all characters
for ($i = $parametersStart; $i < $paramsEnd; $i++) {
$char = $filter[$i];
$isStringStack = $stringStackState !== null;
$isArrayStack = !$isStringStack && $stackCount > 0;
if ($char === static::CHAR_BACKSLASH) {
if (!(static::isSpecialChar($filter[$i + 1]))) {
static::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam);
}
static::appendSymbol($isStringStack, $filter[$i + 1], $i, $filter, $currentParam);
$i++;
continue;
}
// String support + escaping support
if (
(self::isQuote($char)) && // Must be string indicator
($filter[$i - 1] !== static::CHAR_BACKSLASH || $filter[$i - 2] === static::CHAR_BACKSLASH) // Must not be escaped;
) {
if ($isStringStack) {
// Dont mix-up string symbols. Only allow the same as on start
if ($char === $stringStackState) {
// End of string
$stringStackState = null;
}
// Either way, add symbol to builder
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
} else {
// Start of string
$stringStackState = $char;
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
}
continue;
}
// Array support
if (!($isStringStack)) {
if ($char === static::CHAR_BRACKET_START) {
// Start of array
$stack[] = $char;
$stackCount++;
continue;
} elseif ($char === static::CHAR_BRACKET_END) {
// End of array
\array_pop($stack);
$stackCount--;
if (strlen($currentParam)) {
$currentArrayParam[] = $currentParam;
}
$params[] = $currentArrayParam;
$currentArrayParam = [];
$currentParam = "";
continue;
} elseif ($char === static::CHAR_COMMA) { // Params separation support
// If in array stack, dont merge yet, just mark it in array param builder
if ($isArrayStack) {
$currentArrayParam[] = $currentParam;
$currentParam = "";
} else {
// Append from parap builder. Either value, or array
if (empty($currentArrayParam)) {
if (strlen($currentParam)) {
$params[] = $currentParam;
}
$currentParam = "";
}
}
continue;
}
}
// Value, not relevant to syntax
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
}
if (strlen($currentParam)) {
$params[] = $currentParam;
$currentParam = "";
}
$parsedParams = [];
foreach ($params as $param) {
// If array, parse each child separatelly
if (\is_array($param)) {
foreach ($param as $element) {
$arr[] = self::parseValue($element);
}
$parsedParams[] = $arr ?? [];
} else {
$parsedParams[] = self::parseValue($param);
}
}
switch ($method) {
case Query::TYPE_EQUAL:
case Query::TYPE_NOT_EQUAL:
case Query::TYPE_LESSER:
case Query::TYPE_LESSER_EQUAL:
case Query::TYPE_GREATER:
case Query::TYPE_GREATER_EQUAL:
case Query::TYPE_CONTAINS:
case Query::TYPE_SEARCH:
case Query::TYPE_IS_NULL:
case Query::TYPE_IS_NOT_NULL:
case Query::TYPE_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
$attribute = $parsedParams[0] ?? '';
if (count($parsedParams) < 2) {
return new Query($method, $attribute);
}
return new Query($method, $attribute, \is_array($parsedParams[1]) ? $parsedParams[1] : [$parsedParams[1]]);
case Query::TYPE_BETWEEN:
return new Query($method, $parsedParams[0], [$parsedParams[1], $parsedParams[2]]);
case Query::TYPE_SELECT:
return new Query($method, values: $parsedParams[0]);
case Query::TYPE_ORDER_ASC:
case Query::TYPE_ORDER_DESC:
return new Query($method, $parsedParams[0] ?? '');
case Query::TYPE_LIMIT:
case Query::TYPE_OFFSET:
case Query::TYPE_CURSOR_AFTER:
case Query::TYPE_CURSOR_BEFORE:
if (count($parsedParams) > 0) {
return new Query($method, values: [$parsedParams[0]]);
}
return new Query($method);
default:
return new Query($method);
}
}
/**
* Parses value.
*
* @param string $value
* @return mixed
*/
function parseValue(string $value): mixed
{
$value = \trim($value);
if ($value === 'false') { // Boolean value
return false;
} elseif ($value === 'true') {
return true;
} elseif ($value === 'null') { // Null value
return null;
} elseif (\is_numeric($value)) { // Numeric value
// Cast to number
return $value + 0;
} elseif (\str_starts_with($value, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($value, static::CHAR_SINGLE_QUOTE)) { // String param
$value = \substr($value, 1, -1); // Remove '' or ""
return $value;
}
// Unknown format
return $value;
}
/**
* Utility method to only append symbol if relevant.
*
* @param bool $isStringStack
* @param string $char
* @param int $index
* @param string $filter
* @param string $currentParam
* @return void
*/
function appendSymbol(bool $isStringStack, string $char, int $index, string $filter, string &$currentParam): void
{
// Ignore spaces and commas outside of string
$canBeIgnored = false;
if ($char === static::CHAR_SPACE) {
$canBeIgnored = true;
} elseif ($char === static::CHAR_COMMA) {
$canBeIgnored = true;
}
if ($canBeIgnored) {
if ($isStringStack) {
$currentParam .= $char;
}
} else {
$currentParam .= $char;
}
}
function isQuote(string $char): bool
{
if ($char === self::CHAR_SINGLE_QUOTE) {
return true;
} elseif ($char === self::CHAR_DOUBLE_QUOTE) {
return true;
}
return false;
}
function isSpecialChar(string $char): bool
{
if ($char === static::CHAR_COMMA) {
return true;
} elseif ($char === static::CHAR_BRACKET_END) {
return true;
} elseif ($char === static::CHAR_BRACKET_START) {
return true;
} elseif ($char === static::CHAR_DOUBLE_QUOTE) {
return true;
} elseif ($char === static::CHAR_SINGLE_QUOTE) {
return true;
}
return false;
}
}

View file

@ -41,7 +41,6 @@ class V17Test extends TestCase
];
}
/**
* @dataProvider createUpdateRecoveryProvider
*/
@ -53,4 +52,38 @@ class V17Test extends TestCase
$this->assertEquals($expected, $result);
}
public function createQueryProvider()
{
return [
'convert queries' => [
[
'queries' => [
'cursorAfter("exampleId")',
'search("name", ["example"])',
'isNotNull("name")'
]
],
[
'queries' => [
'{"method":"cursorAfter","values":["exampleId"]}',
'{"method":"search","attribute":"name","values":["example"]}',
'{"method":"isNotNull","attribute":"name"}'
]
],
]
];
}
/**
* @dataProvider createQueryProvider
*/
public function testQuery(array $content, array $expected): void
{
$model = 'databases.getDocument';
$result = $this->filter->parse($content, $model);
$this->assertEquals($expected, $result);
}
}