diff --git a/.gitignore b/.gitignore index 8c8076f..a52d582 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea vendor -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +/wp-tests-config.php diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..879d6fe --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,108 @@ +init(); + +class FakeContactModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model { + + protected $type = 'contact'; + + protected $access_cap = 'manage_options'; + + public function fields() { + return array( + 'id' => Field_Type_Validation_Enum::INT, + 'first_name' => Field_Type_Validation_Enum::STRING, + 'last_name' => Field_Type_Validation_Enum::STRING, + 'email' => Field_Type_Validation_Enum::EMAIL, + 'phone' => Field_Type_Validation_Enum::STRING, + 'foobar' => function ( $value ) { + if ( $value === 'foo' ) { + return 'foo'; + } + + return null; + }, + ); + } + + public function meta_fields() { + return array( + 'secondary_phone' => Field_Type_Validation_Enum::STRING, + 'alternate_website' => Field_Type_Validation_Enum::STRING, + ); + } + + public function relationships() { + return new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection(); + } + +} + +class FakeGroupModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model { + + protected $type = 'group'; + + protected $fields = array( + 'label', + ); + + public function fields() { + return array( + 'label' => Field_Type_Validation_Enum::STRING, + ); + } + + protected $access_cap = 'manage_options'; + + public function relationships() { + return new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection( + array( + new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship( 'group', 'contact', 'manage_options' ) + ) + ); + } + +} + +function gravitytools_tests_reset_db() { + echo "\r\n"; + echo '=========================================' . "\r\n"; + echo 'Cleaning up test database for next run...' . "\r\n"; + global $wpdb; + + $tables = array( + 'contact', + 'company', + 'group', + 'deal', + 'pipeline', + 'company_contact', + 'group_contact', + 'deal_company', + 'deal_contact', + 'pipeline_deal', + 'meta', + ); + + foreach( $tables as $table ) { + $table_name = sprintf( '%s%s_%s', $wpdb->prefix, 'gravitycrm', $table ); + $sql = sprintf( 'TRUNCATE TABLE %s', $table_name ); + $wpdb->query( $sql ); + } + + echo 'Done cleaning test database!' . "\r\n"; + echo '=========================================' . "\r\n"; + echo "\r\n"; +} \ No newline at end of file diff --git a/composer.json b/composer.json index a0431d8..5a9ca60 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,10 @@ ], "minimum-stability": "stable", "require-dev": { - "phpunit/phpunit": ">4.0 <7" + "phpunit/phpunit": "7.5.9", + "yoast/phpunit-polyfills": "3.1.1" + }, + "require": { + "lucatume/function-mocker": "~1.0" } } diff --git a/composer.lock b/composer.lock index ab92abb..8bacf65 100644 --- a/composer.lock +++ b/composer.lock @@ -4,39 +4,129 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "abdab76f8abba479276c1fb22de26e19", - "packages": [], - "packages-dev": [ + "content-hash": "14d8a454218984c192568755d0016389", + "packages": [ + { + "name": "antecedent/patchwork", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/antecedent/patchwork.git", + "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/1bf183a3e1bd094f231a2128b9ecc5363c269245", + "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignas Rudaitis", + "email": "ignas.rudaitis@gmail.com" + } + ], + "description": "Method redefinition (monkey-patching) functionality for PHP.", + "homepage": "https://antecedent.github.io/patchwork/", + "keywords": [ + "aop", + "aspect", + "interception", + "monkeypatching", + "redefinition", + "runkit", + "testing" + ], + "support": { + "issues": "https://github.com/antecedent/patchwork/issues", + "source": "https://github.com/antecedent/patchwork/tree/2.2.1" + }, + "time": "2024-12-11T10:19:54+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + }, + "time": "2024-12-07T21:18:45+00:00" + }, { "name": "doctrine/instantiator", - "version": "1.0.5", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1 || ^8.0" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" @@ -50,18 +140,18 @@ { "name": "Marco Pivetta", "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" + "homepage": "https://ocramius.github.io/" } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.0.5" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -77,29 +167,128 @@ "type": "tidelift" } ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "lucatume/args", + "version": "1.0.1.1", + "source": { + "type": "git", + "url": "https://github.com/lucatume/args.git", + "reference": "9ab69f5c995813b2dfbb067100ada500ee2893e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lucatume/args/zipball/9ab69f5c995813b2dfbb067100ada500ee2893e8", + "reference": "9ab69f5c995813b2dfbb067100ada500ee2893e8", + "shasum": "" + }, + "require": { + "xrstf/composer-php52": "1.*" + }, + "require-dev": { + "codeception/codeception": "*" + }, + "type": "library", + "autoload": { + "psr-0": { + "Arg": "src/", + "tad_": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0" + ], + "authors": [ + { + "name": "Luca Tumedei", + "email": "luca@theaveragedev.com" + } + ], + "description": "A PHP 5.2 compatible arguments handling library.", + "support": { + "issues": "https://github.com/lucatume/args/issues", + "source": "https://github.com/lucatume/args/tree/master" + }, + "time": "2018-02-11T12:20:29+00:00" + }, + { + "name": "lucatume/function-mocker", + "version": "1.3.8", + "source": { + "type": "git", + "url": "https://github.com/lucatume/function-mocker.git", + "reference": "97dbec0d806dfa2745fd1f793a86bec9a852629e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lucatume/function-mocker/zipball/97dbec0d806dfa2745fd1f793a86bec9a852629e", + "reference": "97dbec0d806dfa2745fd1f793a86bec9a852629e", + "shasum": "" + }, + "require": { + "antecedent/patchwork": "^2.0", + "lucatume/args": "^1.0", + "php": ">=5.6.0", + "phpunit/phpunit": ">=5.7" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/shims.php", + "src/functions.php" + ], + "psr-0": { + "tad\\FunctionMocker": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "theAverageDev", + "email": "luca@theaveragedev.com" + } + ], + "description": "Function mocking with Patchwork", + "support": { + "issues": "https://github.com/lucatume/function-mocker/issues", + "source": "https://github.com/lucatume/function-mocker/tree/1.3.8" + }, + "time": "2018-04-18T15:25:42+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.7.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^4.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { @@ -124,41 +313,152 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.x" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, - "time": "2017-10-19T19:58:43+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-11-08T17:47:46+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/master" + }, + "time": "2018-07-08T19:19:57+00:00" }, { "name": "phpdocumentor/reflection-common", - "version": "1.0.1", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-2.x": "2.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -182,40 +482,51 @@ ], "support": { "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master" + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" }, - "time": "2017-09-11T18:02:19+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.3.2", + "version": "5.6.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2" + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2", - "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", - "webmozart/assert": "^1.0" + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -226,48 +537,58 @@ { "name": "Mike van Riel", "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/3.x" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" }, - "time": "2017-11-10T14:09:06+00:00" + "time": "2024-12-07T09:39:29+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-1.x": "1.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -280,41 +601,44 @@ "email": "me@mikevanriel.com" } ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/master" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" }, - "time": "2017-07-14T14:27:02+00:00" + "time": "2024-11-09T15:12:26+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.10.3", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "451c3cd1418cf640de218914901e51b064abb093" + "reference": "a0165c648cab6a80311c74ffc708a07bb53ecc93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", - "reference": "451c3cd1418cf640de218914901e51b064abb093", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/a0165c648cab6a80311c74ffc708a07bb53ecc93", + "reference": "a0165c648cab6a80311c74ffc708a07bb53ecc93", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", - "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + "doctrine/instantiator": "^1.2 || ^2.0", + "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.*", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { - "phpspec/phpspec": "^2.5 || ^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + "friendsofphp/php-cs-fixer": "^3.40", + "phpspec/phpspec": "^6.0 || ^7.0", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -342,6 +666,7 @@ "keywords": [ "Double", "Dummy", + "dev", "fake", "mock", "spy", @@ -349,46 +674,93 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" + "source": "https://github.com/phpspec/prophecy/tree/v1.20.0" }, - "time": "2020-03-05T15:02:03+00:00" + "time": "2024-11-19T13:12:41+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", + "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" + }, + "time": "2024-10-13T11:29:49+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.8", + "version": "6.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "^1.3", - "phpunit/php-text-template": "^1.2", - "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "^1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "^1.0 || ^2.0" + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" }, "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^7.0" }, "suggest": { - "ext-xdebug": "^2.5.1" + "ext-xdebug": "^2.6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "6.1-dev" } }, "autoload": { @@ -403,7 +775,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -415,33 +787,35 @@ "xunit" ], "support": { - "irc": "irc://irc.freenode.net/phpunit", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/4.0" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master" }, - "time": "2017-04-02T07:44:40+00:00" + "time": "2018-10-31T16:06:48+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.5", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + "reference": "69deeb8664f611f156a924154985fbd4911eb36b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/69deeb8664f611f156a924154985fbd4911eb36b", + "reference": "69deeb8664f611f156a924154985fbd4911eb36b", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -456,7 +830,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -467,11 +841,16 @@ "iterator" ], "support": { - "irc": "irc://irc.freenode.net/phpunit", "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.6" }, - "time": "2017-11-27T13:52:08+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:39:50+00:00" }, { "name": "phpunit/php-text-template", @@ -520,28 +899,28 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.9", + "version": "2.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a691211e94ff39a34811abd521c31bd5b305b0bb", + "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -556,7 +935,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -567,35 +946,41 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/master" + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.4" }, - "time": "2017-02-26T11:10:40+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:42:41+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.12", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" + "reference": "9c1da83261628cb24b6a6df371b6e312b3954768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", - "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768", + "reference": "9c1da83261628cb24b6a6df371b6e312b3954768", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=5.3.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -620,58 +1005,66 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", - "source": "https://github.com/sebastianbergmann/php-token-stream/tree/1.4" + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3" }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "abandoned": true, - "time": "2017-12-04T08:55:13+00:00" + "time": "2021-07-26T12:15:06+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.27", + "version": "7.5.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c" + "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", - "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/134669cf0eeac3f79bc7f0c793efbc158bffc160", + "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.4", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "^1.2.4", - "sebastian/diff": "^1.4.3", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "^1.1", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "^1.0.6|^2.0.1", - "symfony/yaml": "~2.1|~3.0|~4.0" + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" }, "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" + "phpunit/phpunit-mock-objects": "*" }, "require-dev": { "ext-pdo": "*" }, "suggest": { + "ext-soap": "*", "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -679,7 +1072,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.7.x-dev" + "dev-master": "7.5-dev" } }, "autoload": { @@ -707,87 +1100,22 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/5.7.27" - }, - "time": "2018-02-01T05:50:59+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "3.4.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" + "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.9" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "support": { - "irc": "irc://irc.freenode.net/phpunit", - "issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues", - "source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/3.4" - }, - "abandoned": true, - "time": "2017-06-30T09:13:00+00:00" + "time": "2019-04-19T15:50:46+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" + "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", - "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", + "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", "shasum": "" }, "require": { @@ -821,7 +1149,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.3" }, "funding": [ { @@ -829,34 +1157,34 @@ "type": "github" } ], - "time": "2020-11-30T08:15:22+00:00" + "time": "2024-03-01T13:45:45+00:00" }, { "name": "sebastian/comparator", - "version": "1.2.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dc7ceb4a24aede938c7af2a9ed1de09609ca770", + "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" + "php": ">=7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -869,6 +1197,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -880,14 +1212,10 @@ { "name": "Bernhard Schussek", "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" } ], "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", @@ -895,34 +1223,41 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/1.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5" }, - "time": "2017-01-29T09:50:25+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:31:48+00:00" }, { "name": "sebastian/diff", - "version": "1.4.3", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/98ff311ca519c3aa73ccd3de053bdb377171d7b6", + "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -935,50 +1270,62 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "diff" + "diff", + "udiff", + "unidiff", + "unified diff" ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/1.4" + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.6" }, - "time": "2017-05-22T07:24:03+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:16:36+00:00" }, { "name": "sebastian/environment", - "version": "2.0.0", + "version": "4.2.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + "reference": "56932f6049a0482853056ffd617c91ffcc754205" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/56932f6049a0482853056ffd617c91ffcc754205", + "reference": "56932f6049a0482853056ffd617c91ffcc754205", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^5.0" + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -1005,36 +1352,42 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/master" + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.5" }, - "time": "2016-11-26T07:53:53+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:49:59+00:00" }, { "name": "sebastian/exporter", - "version": "2.0.0", + "version": "3.1.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/1939bc8fd1d39adcfa88c5b35335910869214c56", + "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~2.0" + "php": ">=7.2", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -1047,6 +1400,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1055,17 +1412,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -1076,29 +1429,35 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/master" + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.6" }, - "time": "2016-11-19T08:54:04+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:21:38+00:00" }, { "name": "sebastian/global-state", - "version": "1.1.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^6.0" }, "suggest": { "ext-uopz": "*" @@ -1106,7 +1465,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1131,35 +1490,36 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/1.1.1" + "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0" }, - "time": "2015-10-12T03:26:01+00:00" + "time": "2017-04-27T15:39:26+00:00" }, { "name": "sebastian/object-enumerator", - "version": "2.0.1", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" + "reference": "ac5b293dba925751b808e02923399fb44ff0d541" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/ac5b293dba925751b808e02923399fb44ff0d541", + "reference": "ac5b293dba925751b808e02923399fb44ff0d541", "shasum": "" }, "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~2.0" + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "~5" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -1181,34 +1541,95 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/master" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:54:02+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "1d439c229e61f244ff1f211e5c99737f90c67def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1d439c229e61f244ff1f211e5c99737f90c67def", + "reference": "1d439c229e61f244ff1f211e5c99737f90c67def", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.3" }, - "time": "2017-02-18T15:18:39+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:56:04+00:00" }, { "name": "sebastian/recursion-context", - "version": "2.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/9bfd3c6f1f08c026f542032dfb42813544f7d64c", + "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -1221,14 +1642,14 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, { "name": "Adam Harvey", "email": "aharvey@php.net" @@ -1238,31 +1659,37 @@ "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.2" }, - "time": "2016-11-19T07:33:16+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T14:07:30+00:00" }, { "name": "sebastian/resource-operations", - "version": "1.0.0", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/72a7f7674d053d548003b16ff5a106e7e0e06eee", + "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee", "shasum": "" }, "require": { - "php": ">=5.6.0" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1283,10 +1710,15 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.3" }, - "time": "2015-07-28T20:34:47+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:59:09+00:00" }, { "name": "sebastian/version", @@ -1336,119 +1768,90 @@ "time": "2016-10-03T07:35:21+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.19.0", + "name": "theseer/tokenizer", + "version": "1.2.3", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/aed596913b70fae57be53d86faa2e9ef85a2297b", - "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-ctype": "For best performance" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.19-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.19.0" + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/theseer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2020-10-23T09:01:57+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { - "name": "symfony/yaml", - "version": "v3.4.47", + "name": "webmozart/assert", + "version": "1.11.0", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "88289caa3c166321883f67fe5130188ebbb47094" + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/88289caa3c166321883f67fe5130188ebbb47094", - "reference": "88289caa3c166321883f67fe5130188ebbb47094", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/polyfill-ctype": "~1.8" + "ext-ctype": "*", + "php": "^7.2 || ^8.0" }, "conflict": { - "symfony/console": "<3.4" + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" }, "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "phpunit/phpunit": "^8.5.13" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, "autoload": { "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Webmozart\\Assert\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1456,87 +1859,121 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], "support": { - "source": "https://github.com/symfony/yaml/tree/v3.4.47" + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "xrstf/composer-php52", + "version": "v1.0.21", + "source": { + "type": "git", + "url": "https://github.com/composer-php52/composer-php52.git", + "reference": "670ac996f93792f8de0427605cfef342b3429ff3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer-php52/composer-php52/zipball/670ac996f93792f8de0427605cfef342b3429ff3", + "reference": "670ac996f93792f8de0427605cfef342b3429ff3", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-default": "1.x-dev" } + }, + "autoload": { + "psr-0": { + "xrstf\\Composer52": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "time": "2020-10-24T10:57:07+00:00" - }, + "support": { + "issues": "https://github.com/composer-php52/composer-php52/issues", + "source": "https://github.com/composer-php52/composer-php52" + }, + "time": "2022-06-08T03:23:46+00:00" + } + ], + "packages-dev": [ { - "name": "webmozart/assert", - "version": "1.9.1", + "name": "yoast/phpunit-polyfills", + "version": "3.1.1", "source": { "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "e6381c62c4df51677b657fbac79b79dfce7acdab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/e6381c62c4df51677b657fbac79b79dfce7acdab", + "reference": "e6381c62c4df51677b657fbac79b79dfce7acdab", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<3.9.1" + "php": ">=7.0", + "phpunit/phpunit": "^6.4.4 || ^7.0 || ^8.0 || ^9.0 || ^11.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "yoast/yoastcs": "^3.1.0" }, "type": "library", - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" } }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" } ], - "description": "Assertions to validate method input/output with nice error messages.", + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", "keywords": [ - "assert", - "check", - "validate" + "phpunit", + "polyfill", + "testing" ], "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.9.1" + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2020-07-08T17:02:28+00:00" + "time": "2025-01-12T08:41:37+00:00" } ], "aliases": [], @@ -1546,5 +1983,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.6.0" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 69ff2f8..4f9c9df 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + diff --git a/src/Hermes/Enum/class-field-type-validation-enum.php b/src/Hermes/Enum/class-field-type-validation-enum.php new file mode 100644 index 0000000..26fe408 --- /dev/null +++ b/src/Hermes/Enum/class-field-type-validation-enum.php @@ -0,0 +1,212 @@ + array( self::class, 'validate_string' ), + self::INT => array( self::class, 'validate_int' ), + self::FLOAT => array( self::class, 'validate_float' ), + self::BOOLEAN => array( self::class, 'validate_bool' ), + self::DATE => array( self::class, 'validate_date' ), + self::OBJECT => array( self::class, 'validate_object' ), + self::ARR => array( self::class, 'validate_array' ), + self::EMAIL => array( self::class, 'validate_email' ), + ); + } + + /** + * Validate a given value based on the field type. + * + * @since 1.0 + * + * @param string $type The field type to be used for validation. + * @param mixed $value The actual value to validate. + * + * @return mixed + */ + public static function validate( $type, $value ) { + if ( ! is_string( $type ) && is_callable( $type ) ) { + return call_user_func( $type, $value ); + } + + if ( ! array_key_exists( $type, self::validation_map() ) ) { + $error_string = sprintf( 'Field type %s is not valid.', $type ); + throw new \InvalidArgumentException( $error_string, 480 ); + } + + $validated_value = call_user_func( self::validation_map()[ $type ], $value ); + + return $validated_value; + } + + /** + * Validates a string and adds slashes for DB storage. + * + * @since 1.0 + * + * @param mixed $value The value to validate. + * + * @return string|null + */ + public static function validate_string( $value ) { + if ( ! is_string( $value ) ) { + return null; + } + + return addslashes( $value ); + } + + /** + * Validates an integer. + * + * @since 1.0 + * + * @param mixed $value The value to validate. + * + * @return int|null + */ + public static function validate_int( $value ) { + if ( ! is_numeric( $value ) ) { + return null; + } + + return (integer) $value; + } + + /** + * Validates a float. + * + * @since 1.0 + * + * @param mixed $value The value to validate. + * + * @return float|null + */ + public static function validate_float( $value ) { + if ( ! is_numeric( $value ) ) { + return null; + } + + return (float) $value; + } + + /** + * Validates a boolean. + * + * @since 1.0 + * + * @param mixed $value The value to validate. + * + * @return bool|null + */ + public static function validate_bool( $value ) { + return Booliesh::get( $value, null ); + } + + /** + * Validates a date string. + * + * @since 1.0 + * + * @param mixed $value The value to validate. + * + * @return string|null + */ + public static function validate_date( $value ) { + if ( ! is_string( $value ) ) { + return null; + } + + $check = strtotime( $value ); + + if ( ! $check ) { + return null; + } + + return $value; + } + + /** + * Validates an object. + * + * @since 1.0 + * + * @param mixed $value The value to validate. + * + * @return object|null + */ + public static function validate_object( $value ) { + if ( ! is_object( $value ) ) { + return null; + } + + return $value; + } + + /** + * Validates an array. + * + * @since 1.0 + * + * @param mixed $value The value to validate. + * + * @return array|null + */ + public static function validate_array( $value ) { + if ( ! is_array( $value ) ) { + return null; + } + + return $value; + } + + /** + * Validates an email. + * + * @since 1.0 + * + * @param mixed $value The value to validate. + * + * @return string|null + */ + public static function validate_email( $value ) { + return filter_var( $value, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Models/class-model.php b/src/Hermes/Models/class-model.php new file mode 100644 index 0000000..a709f19 --- /dev/null +++ b/src/Hermes/Models/class-model.php @@ -0,0 +1,95 @@ +type; + } + + /** + * Concrete Models must define the specific Fields and Field Types this + * object type has. Any request regarding this object type will use this + * array to determine if a given field should be evaluated, and how to + * validate it. + * + * In the Database design, tables for this object type should have columns + * that strictly match the field names outlined here. For instance, if a field + * called "first_name" is defined here, a column of "first_name" must exist in + * the DB table for this object. + * + * @return array + */ + public function fields() { + return array(); + } + + /** + * Concrete Models must define the specific Fields and Field Types this object + * type supports for *meta* fields. Meta fields differ from typical fields in that + * they do not require specific columns in the base Database table but are rather stored + * in a meta table. This allows for flexibility in adding new fields to a given object type + * after code has been deployed to production while avoiding having to modify DB tables. + * + * @return array + */ + public function meta_fields() { + return array(); + } + + /** + * Checks the current user's access for this object type.Typically this should + * not be overwritten in the concrete Model, except for cases in which custom + * access functionality is required. + * + * @return bool + */ + public function has_access() { + return current_user_can( $this->access_cap ); + } + +} \ No newline at end of file diff --git a/src/Hermes/README.md b/src/Hermes/README.md new file mode 100644 index 0000000..36352c0 --- /dev/null +++ b/src/Hermes/README.md @@ -0,0 +1,554 @@ +# Project Hermes + +__A Query Architecture for dynamic, performant, client-controlled data handling and processing.__ + +## AJAX, GraphQL, and The Problem of Query Structures + +In our typical product configuration, bespoke endpoints are setup and maintained for each unique type +of query that might come from the Client. Need to get a list of users? That's an endpoint. Get the companies users +belong to? Another endpoint. +Save a setting, get a setting, update a field, get a field - all individual endpoints. + +Oftentimes, this paradigm is more than acceptable. The total number of query types being handled is typically low, and +the +need for net-new endpoints rarely occurs. + +But what about situations in which there are many, many types of data to query, in all sorts of various configurations? +What if the +application is a single-page React app, where all the data comes view queries and nothing exists on page load? What if a +given data type +or object model requires a new piece of data? Suddenly we're not just spinning up unique endpoints for each situation, +but also having to +update/modify/drop database tables as well (something that has caused countless headaches for Plugin products like +ours). + +Enter GraphQL, an alternative Query standard to REST/AJAX. GraphQL differs from traditional REST architecture primarily +in how +query structures and data shapes are controlled. In REST, you retrieve a specific object from a specific endpoint. " +Users" are queried via "/json/users". +"Companies" are queried via "json/companies". If you need both, or need to get Users categorized by a given Company, +it's two separate queries, +while necessitating that values from the first are saved and passed to the second. In simple contexts, this is fine, but +as complexity grows, +so do the headaches around this. + +GraphQL tackles this by having the Server simply establish a structured schema of Object Types, along with their +relationships, fields, and other criteria. The Client can then use this schema to query *anything it needs*, all at a +single endpoint. + +## That's great, but what are you talking about? + +Let's look at a typical example: retrieving a list of companies, the departments at said company, and the employees +assigned to each department. In REST, that +would be 3 separate requests, like the following pseudo-code: + +```js +const companies = request( { dataType: 'company' } ); + +for ( company in companies ) { + const departments = request( { dataType: 'department', companyId: company.id } ); + + for ( department in departments ) { + const employees = request( { dataType: 'employee', departmentId: department.id } ); + department.employees = employees; + } + + company.departments = departments; +} + +return companies; +``` + +Not terrible, but if more nesting/relationships were required, things would quickly balloon out of control. +Not to mention we're making 3 distinct requests to the server, requiring more time and bandwidth. + +Now let's look at how this would work in GraphQL: + +```js +const query = `company { + id, + name, + address, + department { + id, + name, + employeeNames: employee { + id, + first_name, + last_name + } + } + }`; + +const companies = request( { query } ); +``` + +Not only do we reduce the total number of requests to one, we also have +complete, full control over the data shape returned to us. We can even alias the various +field values (like we do here by aliasing `employee` to `employeeNames` ). This results in substantially +decreased overhead for the server, as new data structures don't require any updates. It also makes architecting +the client-side system easier, as dynamic queries can be made simply by traversing the schema provided +by the server and selecting which fields/related objects to query. + +Magic! + +## Monkey wrench: GraphQL is enormé. + +This is all good and well, but how do we take advantage of GraphQL? On the plus side, it's an open-source +system that is actively-maintained and provided by some incredible engineers at Facebook/Meta. + +Unfortunately, it's also absolutely gigantic. Including the library required to run a functioning GraphQL server +in PHP is dozens of additional MB of file bloat, all replete with opportunities for conflicts with other plugins +which may use GraphQL. Includig it in a distributed plugin is a non-starter, full-stop. + +So are we out of luck? + +## Solution: just write our own + +Given the limited file size constraints and variety in hosting platforms, we decided the best approach +would be to adopt GraphQL's syntax and functionality, but hand-roll our own server for parsing GraphQL +queries into SQL statements that we can execute against the database. + +And thus, Project Hermes was born! + +## The Bits and Bobs + +Fundamentally, Hermes consists of four mechanisms: `Models`, `Handlers`, `Tokens`, and `Runners`. +During a given request, the `Models` registered to the system are referenced by a `Handler`, which takes the Query +String +and lexes the string into defined `Tokens`, each of which are classes holding the +structured data needed for the various `Runners` to execute the query. + +That's a lot, but let's look at them one by one: + +### Models: The Backbone of Hermes Data + +`Models` are responsible for describing all of the various object types available for +querying and mutation. A given `Model` defines: + +- The fields available for the object type +- The meta fields available to assign to the object type +- The minimum Capability required for they querying user to access the object type +- The relationships a given object type has to _other_ object types. + +When a query is executed, the registered `Models` are referenced to ensure the data being +requested exists, that the data is available to the user, and the various tables required +for querying. + +Consider the following: + +```php +class FakeContactModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model { + + protected $type = 'contact'; + + protected $access_cap = 'manage_options'; + + public function fields() { + return array( + 'id' => Field_Type_Validation_Enum::INT, + 'first_name' => Field_Type_Validation_Enum::STRING, + 'last_name' => Field_Type_Validation_Enum::STRING, + 'email' => Field_Type_Validation_Enum::EMAIL, + 'phone' => Field_Type_Validation_Enum::STRING, + 'foobar' => function ( $value ) { + if ( $value === 'foo' ) { + return 'foo'; + } + + return null; + }, + ); + } + + public function meta_fields() { + return array( + 'secondary_phone' => Field_Type_Validation_Enum::STRING, + 'alternate_website' => Field_Type_Validation_Enum::STRING, + ); + } + + public function relationships() { + return new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection( + array( + new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship( 'group', 'contact', 'manage_options', true ) + ) + ); + } + +} +``` + +This code would register a new `Model` for an object type named `contact`. It defines several fields +that a `contact` can have, as well as any `meta` fields that might be assignable to it. The `fields` +should exist in the related DB table as columns, while `meta` fields can be added ad-hoc and are stored +in the separate `meta` lookup table. + +Each field, whether meta or local, references a specific Field Validation Type, either by directly referencing +one of the Field Validation Type Enum values, or by providing a custom callback. This determines how values +will be validated and sanitized before being inserted into the database. + +Finally, it establishes a relationship to another `Model`, `Group`. Since the table connects Groups _to_ Contacts, +this `Model` designates the relationship as `reversed`, telling the system to look in the `group_contact` table. + +With this `Model` in-hand, you can register it to a `Model_Collection` by simply using: + +```php +$contact = new FakeContactModel(); +$collection = new Model_Collection(); + +$collection->add( 'contact', $contact ); +``` + +### Handlers and Tokens: Hermes' traffic coordinator + +When a Query is sent to the system (typically through a registered AJAX endpoint or REST route), it's passed to a +`Handler`, either the `Query_Handler` for _query_ calls or +the `Mutation_Handler` for mutation calls (Insert, Update, Delete, Connect). + +This `Handler` then kicks off the process by passing the Query string to a set of `Tokens`, each of which take +the string (or portions of the string) and lex it into usable objects (or `Tokens`) holding all of the data needed +for the specific operation being handled. + +To accomplish this, each `Token` defines a series of key/value pairs, where the `key` represents a regex `MARK` for +identifying the +path taken in the regular expression, and the `value` is the pattern to match against while running the expression. +These are passed to a call to +`preg_match_all()` against the Query string. + +This results in an array of `MARK` designators along with a matching array containing the matched results. These pairs +are evaluated +and either stored to the `Token` as data or (when the `Token` is identified as requiring further lexing) passed along to +another `Token` for +processing. + +### Runners: Hermes' Bravest Little Soldiers + +Once all the various `Tokens` have been generated, we're left with a structured set of PHP Objects, each containing the +data +necessary for performing the Query. With these in hand, the system then passes them on to a series of `Runners`. These +clases +are responsible for processing the stack of `Tokens`, taking their various bits of data, and converting it into SQL +statements +which can then be executed against the Databse. + +Once these SQL statements are executed, the results are collated and - when applicable - the structure outlined +in the original Query request is returned. (Note: Delete and Connect mutations don't have a bespoke return structure. +The response from +such operations will be consistent). + +## Implementing Hermes in New Projects + +Let's go over a sample of how to implement Hermes in a new project. This example will assume that you are +using the standard Service Provider pattern we use in other products, but if not you simple need to move the +code to whatever bootstrapping/provider paradigm you are using. + +### Setting up Models + +First, you need to register a `Model_Collection` to your provider and add some `Models` to it based on what your +data needs are. We'll register a single `Contact` model. + +```php + +$container->add( self::CONTACT_MODEL, function() { + // Assume this Model exists elsewhere in the codebase. + return new Contact_Model; +}); + +$container->add( self::MODEL_COLLECTION, function() use ( $container ) { + $collection = new Model_Collection(); + $collection->add( 'contact', $container->get( self::CONTACT_MODEL ) ); +}); +``` + +### Configuring the Handlers + +Next, we need to configure the `Handlers` for use in this system. To do this, +we'll need a couple pieces: a string representing the `db_namespace` used in this product, +and the `Model_Collection` we registered earlier. The `db_namespace` should be unique to +the product, and is used when generating table names. + +```php +$container->add( self::QUERY_HANDLER, function() use ( $container ) { + $db_namespace = 'gravitytools'; + $model_collection = $container->get( self::MODEL_COLLECTION ); + return new Query_Handler( $db_namespace, $model_collection ); +}); +``` + +The `Mutation_Handler` is a bit more involved, as we'll also need to determine +which Mutation types we want to support, and pass the related `Runners` to our `Handler`. + +Typically you'll need to support Insert, Update, Connect, and Delete operations. + +```php + +$container->add( self::MUTATION_HANDLER, function() use ( $container ) { + $db_namespace = 'gravitytools'; + $model_collection = $container->get( self::MODEL_COLLECTION ); + $query_handler = $container->get( self::QUERY_HANDLER ); + $runners = array( + 'insert' => new Insert_Runner( $db_namespace, $query_handler ), + 'delete' => new Delete_Runner( $db_namespace, $query_handler ), + 'connect' => new Connect_Runner( $db_namespace, $query_handler ), + 'update' => new Update_Runner( $db_namespace, $query_handler ), + ); + return new Mutation_Handler( $db_namespace, $model_collection, $query_handler, $runners ); +}); +``` + +With that, the system now has all the information it needs to start handling Queries! + +### Adding Endpoints + +Finally, you'll need to add endpoints to handle the Query requests. Typically you'll want to +register 2 endpoints, one for Query and one for Mutation, but depending on your needs you may also +use a single endpoint and handle routing internally. For this example, we'll register 2 endpoints via +WordPress's AJAX system. + +```php +add_action( 'wp_ajax_hermes_query', function() use ( $container ) { + $query_string = filter_input( INPUT_POST, 'query', FILTER_DEFAULT ); + + // The handlers call wp_send_json() at the end, so no exit is required. + $container->get( self::QUERY_HANDLER )->handle_query( $query_string ); +} ); + +add_action( 'wp_ajax_hermes_mutation', function() use ( $container ) { + $query_string = filter_input( INPUT_POST, 'mutation', FILTER_DEFAULT ); + + // The handlers call wp_send_json() at the end, so no exit is required. + $container->get( self::MUTATION_HANDLER )->handle_mutation( $query_string ); +} ); + +``` + +With that in place, `POST` requests to `/wp-admin/admin-ajax.php?action=hermes_query` will trigger a Query +request, and `POST` requests to `/wp-admin/admin-ajax.php?action=hermes_mutation` will trigger a Mutation. + +In actual production environments, you'll want to make sure to add in authentication via nonces/keys/some +other mechanism, but at this point you have a functioning Hermes system! + +## Database Table Setup + +In the future, the sytem will reference any registered `Models` and automatically generate DB tables based +on their information. For now, however, the table setup is a manual process. + +The most-important aspect of table setup is that you follow the explicit naming convention required. The convention +is as follows: + +#### For Objects +`$wpdb->prefix + '_' + $db_namespace + '_' + $object_type` + +An object type named `contact` in a product with a namespace of `gravitytools` will need a table named `wp_gravitytools_contact`. + +#### For Lookup Tables +`$wpdb->prefix + '_' + $db_namespace + '_' + $from_object_type + '_' + $to_object_type` + +A table connecting a `contact` to `group` in a product with a namespace of `gravitytools` will need a table named `wp_gravitytools_contact_group`. + +Columns should be `id`, `$from_object_id`, and `$to_object_id`. + +So our `contact` to `group` table would have columns of `id`, `contact_id`, and `group_id`. + +#### For the Meta Table +`$wpdb->prefix + '_' + $db_namespace + '_' + 'meta` + +A meta table in a product with a namespace of `gravitytools` will need a table named `wp_gravitytools_meta`. + +## Using Hermes + +Once you've gotten Hermes set up in your environment (and created the related DB tables), you're ready to start +querying it. + +There are two fundamental types of interactions you can have with Hermes: `query` and `mutation`. + +### Queries + +Queries are the mechanism used to _retrieve_ data from the server. In a REST paradigm, this would be your +`GET` requests. They don't modify any existing data, they simply retrieve data from the server in the shape +sent in the query. + +#### Basic Query + +Let's look at a very basic example. Imagine you need to retrieve all of the `company` records from the server, +and that you want to have the `id`, `name`, and `address` fields returned for each record. The query would look +something like this: + +```graphql +{ + company { + id, + name, + address + } +} +``` + +#### Adding Arguments + +That works great, but it's rare that you need to retrieve every single record of a given type in a single query. +Instead, let's add `limit` as an `argument` so we only retreive the first 10 records: + +```graphql +{ + company( limit: 10 ) { + id, + name, + address + } +} +``` + +`Arguments` can be combined by passing multiple comma-delineated pairs: + +```graphql +{ + company( limit: 10, offset: 5, is_operational: true ) { + id, + name, + address + } +} +``` + +Here we provided three `arguments`: we limite the results to 10, offset the results by 5, and restrict +the results to only those records in which the field `is_operational` is `true`. + +#### Argument Operators + +By default, an argument will be evaluated as `=` (e.g. `limit: 10` becomes `limit = 10` ). Other operator types +can be indicated by suffixing the argument's field name with the operator type you wish to use. See the following table +for all the supported operators. + +| Operator Type | Suffix | Example | +|---------------|-------------|---------------------------| +| `=` | `No Suffix` | `length: 10` | +| `!=` | `_ne` | `length_ne: 10` | +| `>` | `_gt` | `length_gt: 10` | +| `<` | `_lt` | `length_lt: 10` | +| `>=` | `_gte` | `length_gte: 10` | +| `<=` | `_lte` | `length_lte: 10` | +| `IN/CONTAINS` | `_in` | `length_in: 10\|20\|30\|` | + + +#### Related Object Records + +We can also query for records related to the main object type we're retrieving. For instance, if we wanted to +retrieve all of the `departments` in each `company`, we can simply add it like so: + +```graphql +{ + company( limit: 10 ) { + id, + name, + address, + department { + id, + name, + } + } +} +``` + +Queries can be infinitely-nested, and each level can utilize arguments and fields just like a non-nested query. Do note, however, +that higher levels of nesting will result in more-complex queries, and on larger datasets the query time can balloon. Using `limits` and +other `argument` types can help alleviate this, but always use caution when heavily-nesting your queries. + +#### Aliasing + +Sometimes, the field names we use in the database don't work particularly well in client applications. Rather than forcing +the client application to traverse the resulting records and rename fields as needed, we can simply alias the field names directly +in the query: + +```graphql +{ + first_ten_companies: company( limit: 10 ) { + id, + business_name: name, + address + } +} +``` + +This would alias `company` to `first_ten_companies`, and the field `name` to `business_name` in the resulting data. Any level of the query +can be aliased, whether it's a field or a nested query. + +### Mutations + +In addition to retrieving data, we can also use Hermes to modify, create, or remove existing data. These types of operations +are called `mutations`, and come in four varieties. The type of mutation can be defined by prefixing the `object type` with the type +of `mutation` you wish to run. + +#### Insert + +An `insert` mutation inserts a new record into the database. It consists of two parts: the objects to insert, and the values to return once +they have been inserted: + +```graphql +{ + insert_company( objects: [{ name: "My Business", address: "1234 Pine Drive" }]) { + returning { + id, + name, + address + } + } +} +``` + +The above `mutation` would result in our record being added to the database, and the `id`, `name`, and `address` fields being returned for our new record. + +Multiple records can be inserted at a time, and the returned values will include every record created. + +#### Update + +An `update` mutation modifies an existing record with new values. Similar to the `insert` mutation, we also define what data shape we want to be returned +once the update is complete. Unlike `insert` mutations, only a single record can be updated at a time. + +```graphql +{ + update_company( id: 5, name: "My Updated Company Name" ){ + returning { + id, + name, + address + } + } +} +``` + +The above mutation would update the record with an `id` of `5` with the new name `My Updated Company Name`. + +**Note**: every `update` mutation _must_ include an `id` so that the server knows which record to update. Updates +based on other column values are not currently supported. + +#### Delete + +A `delete` mutation simply removes the provided record from the database. No returning data shape is defined, as there +is no data to return. + +```graphql +{ + delete_company( id: 10 ){} +} +``` + +The above `mutation` would result in the company record with an id of `10` being deleted from the database. + +**Note**: as with `update` mutations, you must pass the `id` of the record you wish to delete. + +#### Connect + +A `connect` mutation creates a relationship between two `object types` (if such a relationship is defined in +the appropriate object `models`). We don't define a resulting data shape; instead the server simply returns a +basic success/failure response. + +```graphql +{ + connect_company_department( from : 1, to: 3 ){} +} +``` + +The above `mutation` would result in the `company` with an `id` of `1` to be related to the `department` with +an `id` of `3`. \ No newline at end of file diff --git a/src/Hermes/Runners/class-connect-runner.php b/src/Hermes/Runners/class-connect-runner.php new file mode 100644 index 0000000..aa932ea --- /dev/null +++ b/src/Hermes/Runners/class-connect-runner.php @@ -0,0 +1,65 @@ +from_object(); + $to_object = $mutation->to_object(); + $from_id = $mutation->from_id(); + $to_id = $mutation->to_id(); + + if ( ! $object_model->relationships()->has( $to_object ) ) { + $error_message = sprintf( 'Relationship from %s to %s does not exist.', $from_object, $to_object ); + throw new \InvalidArgumentException( $error_message, 455 ); + } + + if ( ! $object_model->relationships()->get( $to_object )->has_access() ) { + $error_message = sprintf( 'Attempting to access forbidden object type %s.', $to_object ); + throw new \InvalidArgumentException( $error_message, 403 ); + } + + $table_name = sprintf( '%s%s_%s_%s', $wpdb->prefix, $this->db_namespace, $from_object, $to_object ); + + $check_sql = sprintf( 'SELECT * FROM %s WHERE %_id = "%s" AND %s_id = "%s"', $table_name, $from_object, $from_id, $to_object, $to_id ); + + $existing = $wpdb->get_results( $check_sql ); + + if ( ! empty( $existing ) ) { + $response = sprintf( 'Connection from %s ID %s to %s ID %s already exists.', $from_object, $from_id, $to_object, $to_id ); + wp_send_json_success( $response ); + } + + $connect_sql = sprintf( 'INSERT INTO %s ( %s_id, %s_id ) VALUES( "%s", "%s" )', $table_name, $from_object, $to_object, $from_id, $to_id ); + + $wpdb->query( $connect_sql ); + + $response = sprintf( 'Connection from %s ID %s to %s ID %s created.', $from_object, $from_id, $to_object, $to_id ); + + wp_send_json_success( $response ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Runners/class-delete-runner.php b/src/Hermes/Runners/class-delete-runner.php new file mode 100644 index 0000000..2b8928f --- /dev/null +++ b/src/Hermes/Runners/class-delete-runner.php @@ -0,0 +1,36 @@ +id_to_delete(); + $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $object_model->type() ); + $delete_sql = sprintf( 'DELETE FROM %s WHERE id = "%s"', $table_name, $id_to_delete ); + + $wpdb->query( $delete_sql ); + + wp_send_json_success( array( 'deleted_id' => $id_to_delete ) ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Runners/class-insert-runner.php b/src/Hermes/Runners/class-insert-runner.php new file mode 100644 index 0000000..b7a1907 --- /dev/null +++ b/src/Hermes/Runners/class-insert-runner.php @@ -0,0 +1,78 @@ +children(); + $inserted_ids = array(); + + foreach ( $insertion_objects->children() as $object ) { + $fields = $object->children(); + $categorized_fields = $this->categorize_fields( $object_model, $fields ); + $inserted_id = $this->handle_single_insert( $object_model, $categorized_fields ); + $inserted_ids[] = $inserted_id; + } + + $objects_gql = sprintf( '{ %s: %s(id_in: %s){ %s }', $object_model->type(), $object_model->type(), implode( '|', $inserted_ids ), implode( ', ', $mutation->return_fields() ) ); + + $data = $this->query_handler->handle_query( $objects_gql ); + + wp_send_json_success( $data ); + } + + /** + * Helper method to handle an individual insertion action from the array of objects to insert. + * + * @param Model $object_model + * @param array $categorized_fields + * + * @return int + */ + private function handle_single_insert( $object_model, $categorized_fields ) { + global $wpdb; + + $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $object_model->type() ); + $field_list = $this->get_field_name_list_from_fields( $categorized_fields['local'] ); + $values_list = $this->get_field_values_list_from_fields( $categorized_fields['local'] ); + $sql = sprintf( 'INSERT INTO %s (%s) VALUES (%s)', $table_name, $field_list, $values_list ); + + $wpdb->query( $sql ); + $object_id = $wpdb->insert_id; + + if ( ! empty( $categorized_fields['meta'] ) ) { + foreach ( $categorized_fields['meta'] as $key => $value ) { + $meta_table_name = sprintf( '%s%s_meta', $wpdb->prefix, $this->db_namespace ); + $insert_fields_string = 'object_type, object_id, meta_name, meta_value'; + $insert_values_string = sprintf( '"%s", "%s", "%s", "%s"', $object_model->type(), $object_id, $key, $value ); + $meta_sql = sprintf( 'INSERT INTO %s (%s) VALUES (%s)', $meta_table_name, $insert_fields_string, $insert_values_string ); + + $wpdb->query( $meta_sql ); + } + } + + return $object_id; + } +} \ No newline at end of file diff --git a/src/Hermes/Runners/class-runner.php b/src/Hermes/Runners/class-runner.php new file mode 100644 index 0000000..189e997 --- /dev/null +++ b/src/Hermes/Runners/class-runner.php @@ -0,0 +1,180 @@ +prefix value and the DB table name. + * + * Example: passing 'gravitytools' here would result in tables such + * as 'wp_gravitytools_meta', etc. + * + * This is typically defined a single time in a service provider and passed + * to the various classes which need it, such as this Runner. + * + * @var string + */ + protected $db_namespace; + + /** + * The Query Handler is used to form response objects for Insert and Update operations. + * Using it instead of direct SQL calls ensures that aliases, field validation, etc, are + * all applied to the response just like a normal Query would. + * + * @var Query_Handler + */ + protected $query_handler; + + /** + * See property descriptions for more information about these arguments and their usage. + * + * @param string $db_namespace + * @param Query_Handler $query_handler + * + * @return void + */ + public function __construct( $db_namespace, $query_handler ) { + $this->db_namespace = $db_namespace; + $this->query_handler = $query_handler; + } + + /** + * Run the actual mutation action against the database. + * + * Concrete Runners will implement this method in order to handle all + * of the database transactions for this mutation type. + * + * This method should have no return value, as it is intended to be the final + * process called before echoing the JSON to return to the client. As such, this method + * should always end with either wp_json_send_success() or wp_json_send_error() to ensure + * that values are echoed correctly and that the request doesn't continue after processing. + * + * @param Mutation_Token $mutation + * @param Model $object_model + * + * @return void + */ + abstract public function run( $mutation, $object_model ); + + /** + * Helper method to take an array of fields and return a comma-delimited + * list for use in INSERT column statements. + * + * @param array $fields + * + * @return string + */ + protected function get_field_name_list_from_fields( $fields ) { + return implode( ', ', array_keys( $fields ) ); + } + + /** + * Helper method to take an array of fields and return a comma-delimited + * list of their values for use in an INSERT VALUES() statement. + * + * @param array $fields + * + * @return string + */ + protected function get_field_values_list_from_fields( $fields ) { + $values = array_values( $fields ); + foreach ( $values as $key => $value ) { + $values[ $key ] = sprintf( '"%s"', $value ); + } + + return implode( ', ', $values ); + } + + /** + * Helper method to take an array of fields and return a set of + * comma-delimited pairs of 'key = value' strings for usage in + * UPDATE statements. + * + * @param array $fields + * + * @return string + */ + protected function get_update_field_list( $fields ) { + $pairs = array(); + + foreach ( $fields as $key => $value ) { + if ( $key === 'id' ) { + continue; + } + $pairs[] = sprintf( '%s = "%s"', $key, $value ); + } + + return implode( ', ', $pairs ); + } + + /** + * Takes an object model and array of fields to process and categorizes them into + * 'local' (aka, existing in the database as columns) fields and 'meta' fields. This + * also uses the Model to check permissions for the given object type, and ensures + * that all referenced fields are properly defined in the Model. + * + * @param Model $object_model + * @param array $fields_to_process + * + * @return array|array[] + */ + protected function categorize_fields( $object_model, $fields_to_process ) { + $categorized = array( + 'meta' => array(), + 'local' => array(), + ); + + foreach ( $fields_to_process as $field_name => $value ) { + if ( ! array_key_exists( $field_name, $object_model->fields() ) && ! array_key_exists( $field_name, $object_model->meta_fields() ) ) { + $error_string = sprintf( 'Attempting to access invalid field %s on object type %s', $field_name, $object_model->type() ); + throw new \InvalidArgumentException( $error_string, 450 ); + } + + if ( array_key_exists( $field_name, $object_model->fields() ) ) { + $field_validation_type = $object_model->fields()[ $field_name ]; + $validated = Field_Type_Validation_Enum::validate( $field_validation_type, $value ); + + if ( ! is_null( $value ) && is_null( $validated ) ) { + $field_type_string = is_string( $field_validation_type ) ? $field_validation_type : 'callback'; + $error_string = sprintf( 'Invalid field value %s sent to field %s with a type of %s.', $value, $field_name, $field_type_string ); + throw new \InvalidArgumentException( $error_string, 451 ); + } + + $categorized['local'][ $field_name ] = $validated; + } + + if ( array_key_exists( $field_name, $object_model->meta_fields() ) ) { + $field_validation_type = $object_model->meta_fields()[ $field_name ]; + + $validated = Field_Type_Validation_Enum::validate( $field_validation_type, $value ); + + if ( ! is_null( $value ) && is_null( $validated ) ) { + $error_string = sprintf( 'Invalid field value %s sent to field %s with a type of %s.', $value, $field_name, (string) $field_validation_type ); + throw new \InvalidArgumentException( $error_string, 451 ); + } + + $categorized['meta'][ $field_name ] = $validated; + } + } + + return $categorized; + } + +} \ No newline at end of file diff --git a/src/Hermes/Runners/class-update-runner.php b/src/Hermes/Runners/class-update-runner.php new file mode 100644 index 0000000..b668366 --- /dev/null +++ b/src/Hermes/Runners/class-update-runner.php @@ -0,0 +1,84 @@ +children()->children(); + + if ( ! array_key_exists( 'id', $fields_to_update ) ) { + $error_string = sprintf( 'Update mutations must contain an id in the fields list. Fields provided: %s', json_encode( array_keys( $fields_to_update ) ) ); + throw new \InvalidArgumentException( $error_string, 452 ); + } + + $object_id = $fields_to_update['id']; + + $categorized_fields = $this->categorize_fields( $object_model, $fields_to_update ); + + $this->handle_single_update( $object_model, $categorized_fields, $object_id ); + + $objects_gql = sprintf( '{ %s: %s(id: %s){ %s }', $object_model->type(), $object_model->type(), $object_id, implode( ', ', $mutation->return_fields() ) ); + + $data = $this->query_handler->handle_query( $objects_gql ); + + wp_send_json_success( $data ); + } + + /** + * Helper method to handle an individual update action from the array of objects to update. + * + * @param Model $object_model + * @param array $categorized_fields + * + * @return int + */ + private function handle_single_update( $object_model, $categorized_fields, $object_id ) { + global $wpdb; + + $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $object_model->type() ); + $field_list = $this->get_update_field_list( $categorized_fields['local'] ); + $sql = sprintf( 'UPDATE %s SET %s WHERE id = "%s"', $table_name, $field_list, $object_id ); + + $wpdb->query( $sql ); + + if ( ! empty( $categorized_fields['meta'] ) ) { + foreach ( $categorized_fields['meta'] as $key => $value ) { + $meta_table_name = sprintf( '%s%s_meta', $wpdb->prefix, $this->db_namespace ); + + $delete_sql = sprintf( 'DELETE FROM %s WHERE meta_name = "%s" AND object_id = "%s"', $meta_table_name, $key, $object_id ); + $wpdb->query( $delete_sql ); + + $insert_fields_string = 'object_type, object_id, meta_name, meta_value'; + $insert_values_string = sprintf( '"%s", "%s", "%s", "%s"', $object_model->type(), $object_id, $key, $value ); + $meta_sql = sprintf( 'INSERT INTO %s (%s) VALUES (%s)', $meta_table_name, $insert_fields_string, $insert_values_string ); + + $wpdb->query( $meta_sql ); + } + } + + return $object_id; + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Connect/class-connect-mutation-token.php b/src/Hermes/Tokens/Mutations/Connect/class-connect-mutation-token.php new file mode 100644 index 0000000..73298e4 --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Connect/class-connect-mutation-token.php @@ -0,0 +1,178 @@ +from_object; + } + + /** + * Public $to_object accessor. + * + * @return string + */ + public function to_object() { + return $this->to_object; + } + + /** + * Public $from_id accessor. + * + * @return string + */ + public function from_id() { + return $this->connection_ids->from(); + } + + /** + * Public $to_id accessor. + * + * @return string + */ + public function to_id() { + return $this->connection_ids->to(); + } + + /** + * Return the class properties as an array when children() is called. + * + * @return array + */ + public function children() { + return array( + 'from_object' => $this->from_object(), + 'from_id' => $this->from_id(), + 'to_object' => $this->to_object(), + 'to_id' => $this->to_id(), + ); + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + $this->tokenize( $results ); + + return; + } + + /** + * Use the given REGEX matches to generate the appropriate tokens for this mutation. + * + * @param array $parts - An array resulting from preg_match_all() with this class's regex values. + * + * @return void + */ + public function tokenize( $parts ) { + $matches = $parts[0]; + $marks = $parts['MARK']; + $data = array(); + + while ( ! empty( $matches ) ) { + $value = array_shift( $matches ); + $mark_type = array_shift( $marks ); + + switch ( $mark_type ) { + case 'operation_alias': + $objects = str_replace( 'connect_', '', $value ); + $object_parts = explode( '_', $objects ); + + if ( count( $object_parts ) !== 2 ) { + throw new \InvalidArgumentException( 'Error parsing connection object types.', 480 ); + } + + $data['from_object'] = $object_parts[0]; + $data['to_object'] = $object_parts[1]; + $data['alias'] = $value; + break; + case 'arg_group': + $data['connection_ids'] = new Connection_Values_Token( $value ); + break; + case 'alias': + $has_alias = $value; + break; + } + } + + if ( empty( $data['connection_ids'] ) || empty( $data['from_object'] ) ) { + throw new \InvalidArgumentException( 'Connect payload malformed. Check values and try again.', 485 ); + } + + $this->set_properties( $data ); + } + + /** + * Use the parsed tokens data to set the class properties. + * + * @param array $data + * + * @return void + */ + protected function set_properties( $data ) { + $this->alias = $data['alias']; + $this->connection_ids = $data['connection_ids']; + $this->object_type = $data['from_object']; + $this->from_object = $data['from_object']; + $this->to_object = $data['to_object']; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + protected function regex_types() { + return array( + 'operation_alias' => 'connect_[^\(]*', + 'arg_group' => '\([^\)]+\)', + 'alias' => '[_A-Za-z][_0-9A-Za-z]*:', + 'open_bracket' => '{', + 'close_bracket' => '}', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Connect/class-connection-values-token.php b/src/Hermes/Tokens/Mutations/Connect/class-connection-values-token.php new file mode 100644 index 0000000..759c867 --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Connect/class-connection-values-token.php @@ -0,0 +1,106 @@ +from; + } + + /** + * Public accessor for $to. + * + * @return string + */ + public function to() { + return $this->to; + } + + /** + * Return the $to and $from values as children. + * + * @return array + */ + public function children() { + return array( + 'from' => $this->from, + 'to' => $this->to, + ); + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + + if ( count( $results ) < 4 ) { + // Something has gone terrible awry, bail. + return; + } + + $fields = array(); + + $keys = $results[1]; + $values = $results[2]; + + foreach ( $keys as $idx => $key ) { + $value = $values[ $idx ]; + $fields[ $key ] = trim( $value, '"\' ' ); + } + + if ( ! array_key_exists( 'from', $fields ) || ! array_key_exists( 'to', $fields ) ) { + throw new \InvalidArgumentException( 'Connect mutations must provide a from and to ID.', 485 ); + } + + $this->from = $fields['from']; + $this->to = $fields['to']; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + public function regex_types() { + return array( + 'argument_pair' => '([a-zA-z0-9_-]*):([^,\)]+)', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Delete/class-delete-mutation-token.php b/src/Hermes/Tokens/Mutations/Delete/class-delete-mutation-token.php new file mode 100644 index 0000000..6610d1b --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Delete/class-delete-mutation-token.php @@ -0,0 +1,115 @@ +id_to_delete->id(); + } + + /** + * Pass through the children from the $id_to_delete token. + * + * @return array + */ + public function children() { + return $this->id_to_delete->children(); + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + $this->tokenize( $results ); + + return; + } + + /** + * Use the given REGEX matches to generate the appropriate tokens for this mutation. + * + * @param array $parts - An array resulting from preg_match_all() with this class's regex values. + * + * @return void + */ + public function tokenize( $parts ) { + $matches = $parts[0]; + $marks = $parts['MARK']; + $data = array(); + + while ( ! empty( $matches ) ) { + $value = array_shift( $matches ); + $mark_type = array_shift( $marks ); + + switch ( $mark_type ) { + case 'operation_alias': + $data['object_type'] = str_replace( 'delete_', '', $value ); + $data['alias'] = $value; + break; + case 'arg_group': + $data['id_to_delete'] = new ID_To_Delete_Token( $value ); + break; + case 'alias': + $has_alias = $value; + break; + } + } + + if ( empty( $data['id_to_delete'] ) || empty( $data['object_type'] ) ) { + throw new \InvalidArgumentException( 'Delete payload malformed. Check values and try again.', 490 ); + } + + $this->set_properties( $data ); + } + + protected function set_properties( $data ) { + $this->alias = $data['alias']; + $this->object_type = $data['object_type']; + $this->id_to_delete = $data['id_to_delete']; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + protected function regex_types() { + return array( + 'operation_alias' => 'delete_[^\(]*', + 'arg_group' => '\([^\)]+\)', + 'alias' => '[_A-Za-z][_0-9A-Za-z]*:', + 'open_bracket' => '{', + 'close_bracket' => '}', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Delete/class-id-to-delete-token.php b/src/Hermes/Tokens/Mutations/Delete/class-id-to-delete-token.php new file mode 100644 index 0000000..bb37fa9 --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Delete/class-id-to-delete-token.php @@ -0,0 +1,86 @@ +id; + } + + /** + * Return the ID as an array as children. + * + * @return array + */ + public function children() { + return array( $this->id ); + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + + if ( count( $results ) < 4 ) { + // Something has gone terrible awry, bail. + return; + } + + $fields = array(); + + $keys = $results[1]; + $values = $results[2]; + + foreach ( $keys as $idx => $key ) { + $value = $values[ $idx ]; + $fields[ $key ] = trim( $value, '"\' '); + } + + if ( ! array_key_exists( 'id', $fields ) ) { + throw new \InvalidArgumentException( 'Delete operations must provide a valid ID for deletion.', 495 ); + } + + $this->id = $fields['id']; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + public function regex_types() { + return array( + 'argument_pair' => '([a-zA-z0-9_-]*):([^,\)]+)', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Insert/class-insert-mutation-token.php b/src/Hermes/Tokens/Mutations/Insert/class-insert-mutation-token.php new file mode 100644 index 0000000..936d63d --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Insert/class-insert-mutation-token.php @@ -0,0 +1,151 @@ +return_fields; + } + + /** + * Return $objects_to_insert as children. + * + * @return Insertion_Objects_Token + */ + public function children() { + return $this->objects_to_insert; + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + $this->tokenize( $results ); + + return; + } + + /** + * Use the given REGEX matches to generate the appropriate tokens for this mutation. + * + * @param array $parts - An array resulting from preg_match_all() with this class's regex values. + * + * @return void + */ + public function tokenize( $parts ) { + $matches = $parts[0]; + $marks = $parts['MARK']; + $data = array( + 'return_fields' => array(), + ); + + $next_is_return = false; + + while ( ! empty( $matches ) ) { + $value = array_shift( $matches ); + $mark_type = array_shift( $marks ); + + switch ( $mark_type ) { + case 'returning_def': + $next_is_return = true; + break; + case 'operation_alias': + $data['object_type'] = str_replace( 'insert_', '', $value ); + $data['alias'] = $value; + break; + case 'arg_group': + $data['objects_to_insert'] = new Insertion_Objects_Token( $value ); + break; + case 'alias': + $has_alias = $value; + break; + case 'identifier': + if ( ! $next_is_return ) { + break; + } + + $data['return_fields'][] = $value; + break; + case 'close_bracket': + if ( $next_is_return ) { + $next_is_return = false; + break; + } else { + $this->set_properties( $data ); + + return; + } + } + } + + $this->set_properties( $data ); + } + + /** + * Use the parsed tokens data to set the class properties. + * + * @param array $data + * + * @return void + */ + protected function set_properties( $data ) { + $this->alias = $data['alias']; + $this->object_type = $data['object_type']; + $this->objects_to_insert = $data['objects_to_insert']; + $this->return_fields = $data['return_fields']; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + protected function regex_types() { + return array( + 'returning_def' => 'returning', + 'operation_alias' => 'insert_[^\(]*', + 'arg_group' => '\([^\)]+\)', + 'alias' => '[_A-Za-z][_0-9A-Za-z]*:', + 'identifier' => '[_A-Za-z][_0-9A-Za-z]*', + 'open_bracket' => '{', + 'close_bracket' => '}', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Insert/class-insertion-object-token.php b/src/Hermes/Tokens/Mutations/Insert/class-insertion-object-token.php new file mode 100644 index 0000000..16cd3f2 --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Insert/class-insertion-object-token.php @@ -0,0 +1,84 @@ +items; + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $parts ); + + $matches = $parts[0]; + $marks = $parts['MARK']; + + $fields = array(); + $key = false; + + while ( ! empty( $matches ) ) { + $value = array_shift( $matches ); + $mark_type = array_shift( $marks ); + + switch ( $mark_type ) { + case 'key': + $key = trim( $value, ': ' ); + break; + case 'value': + if ( $key ) { + $fields[ $key ] = trim( $value, ': "' ); + $key = false; + } + break; + default: + break; + } + } + + $this->items = $fields; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + public function regex_types() { + return array( + 'value' => ':[^,\}]+', + 'key' => '[a-zA-Z0-9_\-]+', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Insert/class-insertion-objects-token.php b/src/Hermes/Tokens/Mutations/Insert/class-insertion-objects-token.php new file mode 100644 index 0000000..3b73e6a --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Insert/class-insertion-objects-token.php @@ -0,0 +1,73 @@ +objects; + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $parts ); + + $matches = $parts[0]; + $marks = $parts['MARK']; + $objects = array(); + + while ( ! empty( $matches ) ) { + $value = array_shift( $matches ); + $mark_type = array_shift( $marks ); + + switch( $mark_type ) { + case 'object': + $objects[] = new Insertion_Object_Token( $value ); + break; + default: + break; + } + } + + $this->objects = $objects; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + public function regex_types() { + return array( + 'object' => '\{[^\}]+\}', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Update/class-fields-to-update-token.php b/src/Hermes/Tokens/Mutations/Update/class-fields-to-update-token.php new file mode 100644 index 0000000..e65480b --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Update/class-fields-to-update-token.php @@ -0,0 +1,82 @@ +items; + } + + /** + * Return $items as children. + * + * @return array + */ + public function children() { + return $this->items(); + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + + if ( count( $results ) < 4 ) { + // Something has gone terrible awry, bail. + return; + } + + $fields = array(); + + $keys = $results[1]; + $values = $results[2]; + + foreach ( $keys as $idx => $key ) { + $value = $values[ $idx ]; + $fields[ $key ] = trim( $value, '"\' '); + } + + $this->items = $fields; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + public function regex_types() { + return array( + 'argument_pair' => '([a-zA-z0-9_-]*):([^,\)]+)', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/Update/class-update-mutation-token.php b/src/Hermes/Tokens/Mutations/Update/class-update-mutation-token.php new file mode 100644 index 0000000..0174cf8 --- /dev/null +++ b/src/Hermes/Tokens/Mutations/Update/class-update-mutation-token.php @@ -0,0 +1,160 @@ +return_fields; + } + + /** + * Public acessor for $fields_to_update. + * + * @return Fields_To_Update_Token + */ + public function fields_to_update() { + return $this->fields_to_update; + } + + /** + * Return $fields_to_update as children. + * + * @return Fields_To_Update_Token + */ + public function children() { + return $this->fields_to_update; + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + $this->tokenize( $results ); + + return; + } + + /** + * Use the given REGEX matches to generate the appropriate tokens for this mutation. + * + * @param array $parts - An array resulting from preg_match_all() with this class's regex values. + * + * @return void + */ + public function tokenize( $parts ) { + $matches = $parts[0]; + $marks = $parts['MARK']; + $data = array( + 'return_fields' => array(), + ); + + $next_is_return = false; + + while ( ! empty( $matches ) ) { + $value = array_shift( $matches ); + $mark_type = array_shift( $marks ); + + switch ( $mark_type ) { + case 'returning_def': + $next_is_return = true; + break; + case 'operation_alias': + $data['object_type'] = str_replace( 'update_', '', $value ); + $data['alias'] = $value; + break; + case 'arg_group': + $data['fields_to_update'] = new Fields_To_Update_Token( $value ); + break; + case 'alias': + $has_alias = $value; + break; + case 'identifier': + if ( ! $next_is_return ) { + break; + } + + $data['return_fields'][] = $value; + break; + case 'close_bracket': + if ( $next_is_return ) { + $next_is_return = false; + break; + } else { + $this->set_properties( $data ); + + return; + } + } + } + + $this->set_properties( $data ); + } + + /** + * Use the parsed tokens data to set the class properties. + * + * @param array $data + * + * @return void + */ + protected function set_properties( $data ) { + $this->alias = $data['alias']; + $this->object_type = $data['object_type']; + $this->fields_to_update = $data['fields_to_update']; + $this->return_fields = $data['return_fields']; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + protected function regex_types() { + return array( + 'returning_def' => 'returning', + 'operation_alias' => 'update_[^\(]*', + 'arg_group' => '\([^\)]+\)', + 'alias' => '[_A-Za-z][_0-9A-Za-z]*:', + 'identifier' => '[_A-Za-z][_0-9A-Za-z]*', + 'open_bracket' => '{', + 'close_bracket' => '}', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/Mutations/class-generic-mutation-token.php b/src/Hermes/Tokens/Mutations/class-generic-mutation-token.php new file mode 100644 index 0000000..4ce214b --- /dev/null +++ b/src/Hermes/Tokens/Mutations/class-generic-mutation-token.php @@ -0,0 +1,128 @@ +mutation_type; + } + + /** + * Public accessor for $typed_token. + * + * @return Mutation_Token + */ + public function mutation() { + return $this->typed_token; + } + + /** + * Return the $typed_token as children. + * + * @return Mutation_Token + */ + public function children() { + return $this->typed_token; + } + + /** + * Parse the string contents to values. + * + * @param string $contents + * + * @return void + */ + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $parts ); + $matches = $parts[0]; + $marks = $parts['MARK']; + $typed_token = false; + + while ( ! empty( $matches ) ) { + $value = array_shift( $matches ); + $mark_type = array_shift( $marks ); + + if ( $mark_type !== 'operation' ) { + continue; + } + + $cleaned = trim( $value, '{ (' ); + + if ( strpos( $cleaned, 'insert_' ) !== false ) { + $typed_token = new Insert_Mutation_Token( $contents ); + $this->mutation_type = 'insert'; + } + + if ( strpos( $cleaned, 'update_' ) !== false ) { + $typed_token = new Update_Mutation_Token( $contents ); + $this->mutation_type = 'update'; + } + + if ( strpos( $cleaned, 'delete_' ) !== false ) { + $typed_token = new Delete_Mutation_Token( $contents ); + $this->mutation_type = 'delete'; + } + + if ( strpos( $cleaned, 'connect_' ) !== false ) { + $typed_token = new Connect_Mutation_Token( $contents ); + $this->mutation_type = 'connect'; + } + + } + + if ( empty( $typed_token ) ) { + throw new \InvalidArgumentException( 'Invalid operation type passed to mutation.', 475 ); + } + + $this->typed_token = $typed_token; + } + + /** + * The regex types to use while parsing. + * + * $key represents the MARK type, while the $value represents the REGEX string to use. + * + * @return string[] + */ + public function regex_types() { + return array( + 'operation' => '\{[^\(]+\(', + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/class-arguments-token.php b/src/Hermes/Tokens/class-arguments-token.php new file mode 100644 index 0000000..562ab66 --- /dev/null +++ b/src/Hermes/Tokens/class-arguments-token.php @@ -0,0 +1,82 @@ + '>=', + 'lte' => '<=', + 'ne' => '!=', + 'gt' => '>', + 'lt' => '<', + 'in' => 'in', + ); + + public function items() { + return $this->items; + } + + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + + if ( count( $results ) < 4 ) { + // Something has gone terrible awry, bail. + return; + } + + $arguments = array(); + + $keys = $results[1]; + $values = $results[2]; + + foreach ( $keys as $idx => $key ) { + $condition_data = $this->get_condition_data_from_key( $key ); + $value = $values[ $idx ]; + $arguments[] = array( + 'key' => $condition_data['key'], + 'comparator' => $condition_data['comparator'], + 'value' => trim( $value ), + ); + } + + $this->items = $arguments; + } + + public function regex_types() { + return array( + 'argument_pair' => '([a-zA-z0-9_-]*):([^,\)]+)', + ); + } + + public function children() { + return $this->items(); + } + + private function get_condition_data_from_key( $key_to_check ) { + $comparator = '='; + $key = $key_to_check; + + foreach ( $this->comparator_strings as $check => $result ) { + if ( strpos( $key_to_check, $check ) !== false ) { + $key = str_replace( '_' . $check, '', $key_to_check ); + $comparator = $result; + + return array( + 'comparator' => $comparator, + 'key' => $key, + ); + } + } + + return array( + 'comparator' => $comparator, + 'key' => $key, + ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/class-base-token.php b/src/Hermes/Tokens/class-base-token.php new file mode 100644 index 0000000..e70fe94 --- /dev/null +++ b/src/Hermes/Tokens/class-base-token.php @@ -0,0 +1,25 @@ +type; + } + + public function parent() { + return $this->parent; + } + + public function set_parent( Base_Token $parent ) { + $this->parent = $parent; + } + + abstract public function children(); + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/class-data-object-from-array-token.php b/src/Hermes/Tokens/class-data-object-from-array-token.php new file mode 100644 index 0000000..249c47c --- /dev/null +++ b/src/Hermes/Tokens/class-data-object-from-array-token.php @@ -0,0 +1,109 @@ +object_type; + } + + public function arguments() { + if ( ! empty( $this->arguments ) ) { + return $this->arguments->items(); + } + + return array(); + } + + public function fields() { + return $this->fields; + } + + public function alias() { + return $this->alias; + } + + public function parse( &$matches, &$marks, $additional_args = array() ) { + $data = array( + 'object_type' => $additional_args['object_type'], + 'alias' => trim( $additional_args['alias'], ' :' ), + 'arguments' => false, + 'fields' => array(), + ); + + $has_alias = false; + + while ( ! empty( $matches ) ) { + $value = array_shift( $matches ); + $mark_type = array_shift( $marks ); + + switch ( $mark_type ) { + case 'arg_group': + $data['arguments'] = new Arguments_Token( $value ); + $data['arguments']->set_parent( $this ); + break; + case 'alias': + $has_alias = $value; + break; + case 'identifier': + if ( $marks[0] === 'open_bracket' || ( $marks[0] === 'arg_group' && $marks[1] === 'open_bracket' ) ) { + $data_object = new self( $matches, $marks, array( + 'object_type' => $value, + 'alias' => $has_alias + ) ); + $data_object->set_parent( $this ); + $data['fields'][] = $data_object; + $has_alias = false; + break; + } + + $field_data = array( + 'name' => $value, + 'alias' => trim( $has_alias, ' :' ), + ); + + if ( $marks[0] === 'arg_group' ) { + $args = array_shift( $matches ); + $mark = array_shift( $marks ); + $field_data['arguments'] = new Arguments_Token( $args ); + $field_data['arguments']->set_parent( $this ); + } + + $field_token = new Field_Token( $matches, $marks, $field_data ); + $field_token->set_parent( $this ); + $data['fields'][] = $field_token; + $has_alias = false; + break; + case 'close_bracket': + $this->set_properties( $data ); + + return; + } + } + + $this->set_properties( $data ); + } + + private function set_properties( $data ) { + $this->fields = $data['fields']; + $this->arguments = $data['arguments']; + $this->object_type = $data['object_type']; + $this->alias = $data['alias']; + } + + public function children() { + return $this->fields(); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/class-field-token.php b/src/Hermes/Tokens/class-field-token.php new file mode 100644 index 0000000..9540a36 --- /dev/null +++ b/src/Hermes/Tokens/class-field-token.php @@ -0,0 +1,44 @@ +name; + } + + public function alias() { + return $this->alias; + } + + public function object_type() { + return $this->name; + } + + public function arguments() { + return $this->arguments; + } + + public function parse( &$matches, &$marks, $additional_args = array() ) { + $this->name = $additional_args['name']; + $this->alias = $additional_args['alias']; + + if ( isset( $additional_args['arguments'] ) ) { + $this->arguments = $additional_args['arguments']; + } + } + + public function children() { + return $this->arguments(); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/class-mutation-token.php b/src/Hermes/Tokens/class-mutation-token.php new file mode 100644 index 0000000..cf9543f --- /dev/null +++ b/src/Hermes/Tokens/class-mutation-token.php @@ -0,0 +1,27 @@ +object_type; + } + + public function operation() { + return $this->operation; + } + + public function alias() { + return $this->alias; + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/class-query-token.php b/src/Hermes/Tokens/class-query-token.php new file mode 100644 index 0000000..5d97d98 --- /dev/null +++ b/src/Hermes/Tokens/class-query-token.php @@ -0,0 +1,67 @@ +object_type; + } + + public function items() { + return $this->items; + } + + public function alias() { + return $this->alias; + } + + public function parse( $contents ) { + preg_match_all( $this->get_parsing_regex(), $contents, $results ); + + $values = $this->tokenize( $results ); + + $this->object_type = 'query'; + $this->items = $values->fields(); + } + + private function tokenize( $parts ) { + $matches = $parts[0]; + $marks = $parts['MARK']; + $data = array(); + + $data = new Data_Object_From_Array_Token( $matches, $marks, array( 'object_type' => $this->type, 'alias' => false ) ); + $data->set_parent( $this ); + + return $data; + } + + protected function regex_types() { + //(?| (*MARK:arg_group)\([^\)]+\) | (*MARK:identifier)[_A-Za-z][_0-9A-Za-z]* | (*MARK:open_bracket){ | (*MARK:close_bracket)} ) + return array( + 'arg_group' => '\([^\)]+\)', + 'alias' => '[_A-Za-z][_0-9A-Za-z]*:', + 'identifier' => '[_A-Za-z][_0-9A-Za-z]*', + 'open_bracket' => '{', + 'close_bracket' => '}', + ); + } + + /** + * @return Data_Object_From_Array_Token[] + */ + public function children() { + return $this->items(); + } + +} \ No newline at end of file diff --git a/src/Hermes/Tokens/class-token-from-array.php b/src/Hermes/Tokens/class-token-from-array.php new file mode 100644 index 0000000..334405c --- /dev/null +++ b/src/Hermes/Tokens/class-token-from-array.php @@ -0,0 +1,12 @@ +parse( $matches, $marks, $additional_args ); + } + + abstract public function parse( &$matches, &$marks, $additional_args = array() ); +} \ No newline at end of file diff --git a/src/Hermes/Tokens/class-token.php b/src/Hermes/Tokens/class-token.php new file mode 100644 index 0000000..64d1643 --- /dev/null +++ b/src/Hermes/Tokens/class-token.php @@ -0,0 +1,28 @@ +parse( $contents ); + } + + protected function regex_types() { + return array(); + } + + protected function get_parsing_regex() { + $clauses = array(); + + foreach ( $this->regex_types() as $type => $pattern ) { + $clauses[] = sprintf( '(*MARK:%s)%s', $type, $pattern ); + } + + $clauses_concat = implode( '|', $clauses ); + + return sprintf( '/(?|%s)/m', $clauses_concat ); + } + + abstract public function parse( $contents ); +} \ No newline at end of file diff --git a/src/Hermes/Utils/class-model-collection.php b/src/Hermes/Utils/class-model-collection.php new file mode 100644 index 0000000..81fb35e --- /dev/null +++ b/src/Hermes/Utils/class-model-collection.php @@ -0,0 +1,69 @@ +models[ $type ] = $model; + } + + + /** + * Unregister/remove a Model type. + * + * @param string $type + * + * @return void + */ + public function remove( $type ) { + unset( $this->models[ $type ] ); + } + + /** + * Check if a Model of a given type exists in the collection. + * + * @param string $type + * + * @return bool + */ + public function has( $type ) { + return array_key_exists( $type, $this->models ); + } + + /** + * Get a Model from the collection by its type. + * + * @param string $type + * + * @return Model|null + */ + public function get( $type ) { + if ( ! $this->has( $type ) ) { + return null; + } + + return $this->models[ $type ]; + } + +} \ No newline at end of file diff --git a/src/Hermes/Utils/class-relationship-collection.php b/src/Hermes/Utils/class-relationship-collection.php new file mode 100644 index 0000000..d323143 --- /dev/null +++ b/src/Hermes/Utils/class-relationship-collection.php @@ -0,0 +1,69 @@ +relationships = $relationships; + } + + /** + * Add a Relationship to the collection. + * + * @param Relationship $relationship + * + * @return void + */ + public function add( Relationship $relationship ) { + $this->relationships[] = $relationship; + } + + /** + * Get a given Relationship by the related object. + * + * @param string $related_object + * + * @return Relationship|mixed|null + */ + public function get( $related_object ) { + if ( ! $this->has( $related_object ) ) { + return null; + } + + $relationship = array_filter( $this->relationships, function ( $relationship ) use ( $related_object ) { + return $relationship->to() === $related_object; + } ); + + return array_shift( $relationship ); + } + + /** + * Check if the collection contains a relationship for the given object type. + * + * @param string $related_object + * + * @return bool + */ + public function has( $related_object ) { + $relationship = array_filter( $this->relationships, function ( $relationship ) use ( $related_object ) { + return $relationship->to() === $related_object; + } ); + + return ! empty( $relationship ); + } + +} \ No newline at end of file diff --git a/src/Hermes/Utils/class-relationship.php b/src/Hermes/Utils/class-relationship.php new file mode 100644 index 0000000..7bcb0ed --- /dev/null +++ b/src/Hermes/Utils/class-relationship.php @@ -0,0 +1,125 @@ +from = $from; + $this->to = $to; + $this->cap = $cap; + $this->is_reverse = $is_reverse; + } + + /** + * Public $from accessor. + * + * @return string + */ + public function from() { + return $this->from; + } + + /** + * Public $to accessor. + * + * @return string + */ + public function to() { + return $this->to; + } + + + /** + * Public $cap accessor. + * + * @return string + */ + public function cap() { + return $this->cap; + } + + /** + * Whether the current user can access this relationship. (Uses current_user_can() by default). + * + * @return bool + */ + public function has_access() { + return current_user_can( $this->cap ); + } + + /** + * Determines the correct table suffix when querying lookup tables. + * + * When $is_reverse is `true`, the suffix has the object type slugs swapped. + * + * @return string + */ + public function get_table_suffix() { + if ( $this->is_reverse ) { + return sprintf( '%s_%s', $this->to, $this->from ); + } + + return sprintf( '%s_%s', $this->from, $this->to ); + } + +} \ No newline at end of file diff --git a/src/Hermes/class-mutation-handler.php b/src/Hermes/class-mutation-handler.php new file mode 100644 index 0000000..dbf2d23 --- /dev/null +++ b/src/Hermes/class-mutation-handler.php @@ -0,0 +1,146 @@ +prefix + * value and before the actual table name. + * + * Example: + * + * Passing `gravitytools` would result in a meta table name of `wp_gravitytools_meta`.. + * + * @var string + */ + protected $db_namespace; + + /** + * The collection of models supported for queries. + * + * @var Model_Collection + */ + protected $models; + + /** + * A valid Query Handler for retrieving the resulting objects after an insert/update. + * + * @var Query_Handler + */ + protected $query_handler; + + /** + * The Runner for handling Insert mutations. + * + * @var Insert_Runner + */ + protected $insert_runner; + + /** + * The Runner for handling Delete mutations. + * + * @var Delete_Runner + */ + protected $delete_runner; + + /** + * The Runner for handling Update mutations. + * + * @var Update_Runner + */ + protected $update_runner; + + /** + * The Runner for haldling Connect mutations. + * + * @var Connect_Runner + */ + protected $connect_runner; + + /** + * Constructor + * + * @param string $db_namespace + * @param Model_Collection $models + * @param Query_Handler $query_handler + * @param Runner[] $runners + */ + public function __construct( $db_namespace, $models, $query_handler, $runners ) { + $this->db_namespace = $db_namespace; + $this->models = $models; + $this->query_handler = $query_handler; + $this->insert_runner = $runners['insert']; + $this->delete_runner = $runners['delete']; + $this->update_runner = $runners['update']; + $this->connect_runner = $runners['connect']; + } + + /** + * Parse the provided Mutation string and execute the appropriate SQL queries to perform the + * mutation. + * + * @param string $mutation_string + * + * @return void + */ + public function handle_mutation( $mutation_string ) { + global $wpdb; + + + // Pass the string to the Generic Mutation token to determine the specific mutation type. + $generic_mutation = new Generic_Mutation_Token( $mutation_string ); + + /** + * Mutation_Token $mutation + */ + $mutation = $generic_mutation->mutation(); + + // Ensure the object type in question is registered in our Model Collection. + if ( ! $this->models->has( $mutation->object_type() ) ) { + $error_message = sprintf( 'Mutation attempted with invalid object type: %s', $mutation->object_type() ); + throw new \InvalidArgumentException( $error_message, 470 ); + } + + $object_model = $this->models->get( $mutation->object_type() ); + + // Ensure the querying user has the appropriate permissions to access data for this object. + if ( ! $object_model->has_access() ) { + $error_message = sprintf( 'Access not allowed for object type %s', $mutation->object_type() ); + throw new \InvalidArgumentException( $error_message, 403 ); + } + + // Handle the actual mutation based on the identified mutation type by calling its appropriate Runner. + switch ( $mutation->operation() ) { + case 'insert': + $this->insert_runner->run( $mutation, $object_model ); + break; + case 'update': + $this->update_runner->run( $mutation, $object_model ); + break; + case 'delete': + $this->delete_runner->run( $mutation, $object_model ); + break; + case 'connect': + $this->connect_runner->run( $mutation, $object_model ); + break; + default: + break; + } + } + +} \ No newline at end of file diff --git a/src/Hermes/class-query-handler.php b/src/Hermes/class-query-handler.php new file mode 100644 index 0000000..e2ca23e --- /dev/null +++ b/src/Hermes/class-query-handler.php @@ -0,0 +1,477 @@ +prefix + * value and before the actual table name. + * + * Example: + * + * Passing `gravitytools` would result in a meta table name of `wp_gravitytools_meta`.. + * + * @var string + */ + protected $db_namespace; + + /** + * The collection of models supported for queries. + * + * @var Model_Collection + */ + protected $models; + + + /** + * Constructor + * + * @param string $db_namespace + * @param Model_Collection $models + */ + public function __construct( $db_namespace, Model_Collection $models ) { + $this->db_namespace = $db_namespace; + $this->models = $models; + } + + /** + * Parse the given query string text and perform the appropriate database queries to return + * the requested data structure. + * + * @param string $query_string + * + * @return array + */ + public function handle_query( $query_string ) { + global $wpdb; + + // Parse to token array + $query_token = new Query_Token( $query_string ); + $data = array(); + + // Use the Token to generate recursive SQL. + foreach ( $query_token->children() as $object ) { + $object_name = ! empty( $object->alias() ) ? $object->alias() : $object->object_type(); + $sql = $this->recursively_generate_sql( $object ); + $data[ $object_name ] = sprintf( 'SELECT %s', $sql ); + } + + $results = array(); + + // Decode the results and set them up for return. + foreach ( $data as $data_group_name => $query_to_execute ) { + $query_results = $wpdb->get_results( $query_to_execute, ARRAY_A ); + $rows = array(); + foreach ( $query_results as $query_result ) { + $json_data = array_shift( $query_result ); + $decoded_data = json_decode( $json_data, true ); + $rows[] = $decoded_data; + } + + $results[ $data_group_name ] = $rows; + } + + wp_send_json_success( $results ); + } + + /** + * Loops through the objects in the Query String and recursively generates the appropriate + * SQL for the related query. Supports an infinite level of nesting, but caution should be used when + * performing deeply-nested queries as performance may be impacted. + * + * @param Data_Object_From_Array_Token $data + * @param $idx_prefix + * @param $parent_table + * @param $parent_object_type + * + * @return string + */ + public function recursively_generate_sql( Data_Object_From_Array_Token $data, $idx_prefix = null, $parent_table = false, $parent_object_type = false ) { + + // Set up variables + global $wpdb; + + $sql = ''; + + $meta_fields = array(); + $local_fields = array(); + + $object_type = $data->object_type(); + $object_name = ! empty( $data->alias() ) ? $data->alias() : $data->object_type(); + + // Ensure the object type being queried exists as a Model. + if ( ! $this->models->has( $object_type ) ) { + $error_message = sprintf( 'Attempting to access invalid object type %s', $object_type ); + throw new \InvalidArgumentException( $error_message, 460 ); + } + + $object_model = $this->models->get( $object_type ); + + // Ensure that the querying user has the appropriate permissions to access object. + if ( ! $object_model->has_access() ) { + $error_message = sprintf( 'Access not allowed for object type %s', $object_type ); + throw new \InvalidArgumentException( $error_message, 403 ); + } + + // Set up values for the table being queried. + $table_name = $this->compose_table_name( $object_type ); + $table_alias = $this->compose_table_alias( $object_name, $parent_table ); + + // Categorized queried fields as either local or meta fields for future processing. + $fields_to_process = $data->fields(); + $categorized_fields = $this->categorize_fields( $object_model, $fields_to_process, $table_alias ); + + $arguments = $data->arguments(); + + // Set up data arrays for holding pieces of the SQL statement for later concatenation. + $field_pairs = array(); + $where_clauses = array(); + $join_clauses = array(); + + $field_sql = null; + $from_sql = null; + $join_sql = null; + $where_sql = null; + $group_sql = null; + $limit_sql = null; + $separator_sql = null; + + // Arguments are present; parse them and add them to the appropriate SQL arrays. + if ( ! empty( $arguments ) ) { + $this->get_where_clauses_from_arguments( $where_clauses, $table_alias, $arguments ); + $limit_sql = $this->get_limit_from_arguments( $arguments ); + } + + // Loop through each local field and generate the appropriate SQL chunks for retrieving the data. + foreach ( $categorized_fields['local'] as $field_name => $field_alias ) { + if ( is_a( $field_alias, Data_Object_From_Array_Token::class ) ) { + $this_alias = empty( $field_alias->alias() ) ? $field_alias->object_type() : $field_alias->alias(); + $sub_sql = $this->recursively_generate_sql( $field_alias, null, $table_alias, $object_type ); + $sub_sql_parts = explode( '|gsmtpfieldsseparator|', $sub_sql ); + $sub_sql = sprintf( '( SELECT JSON_ARRAYAGG( %s ) %s )', $sub_sql_parts[0], $sub_sql_parts[1] ); + $field_pairs[] = sprintf( '"%s", %s', $this_alias, $sub_sql ); + continue; + } + + $field_pairs[] = sprintf( '"%s", %s.%s', $field_alias, $table_alias, $field_name ); + } + + $meta_table_name = $this->compose_table_name( 'meta' ); + + // Loop through each meta field and compose the appropriate JOIN query for gathering its data. + foreach ( $categorized_fields['meta'] as $field_name => $field_data ) { + $value_clause = $parent_table ? sprintf( '%s.meta_value', $field_data['lookup_table_alias'] ) : sprintf( 'MIN(%s.meta_value)', $field_data['lookup_table_alias'] ); + $field_pairs[] = sprintf( '"%s", %s', $field_data['alias'], $value_clause, $field_name ); + $join_clauses[] = sprintf( 'LEFT JOIN %s AS %s ON %s.object_type = "%s" AND %s.meta_name = "%s" AND %s.object_id = %s.id', + $meta_table_name, + $field_data['lookup_table_alias'], + $field_data['lookup_table_alias'], + $object_type, + $field_data['lookup_table_alias'], + $field_name, + $field_data['lookup_table_alias'], + $table_alias + ); + } + + // A parent table exists, meaning this is a nested query and requires a JOIN statement relating it + // to the parent table. + if ( $parent_table ) { + $parent_model = $this->models->get( $parent_object_type ); + $relationship = $parent_model->relationships()->get( $object_type ); + $lookup_table_name = $this->compose_join_table_name( $relationship->get_table_suffix() ); + $lookup_table_alias = sprintf( 'join_%s', $table_alias ); + $id_string = sprintf( '%s_id', $object_type ); + $parent_id_string = sprintf( '%s_id', $parent_object_type ); + $join_clauses[] = sprintf( 'LEFT JOIN %s AS %s ON %s.id = %s.%s', $lookup_table_name, $lookup_table_alias, $table_alias, $lookup_table_alias, $id_string ); + $where_clauses[] = sprintf( '%s.%s = %s.id', $lookup_table_alias, $parent_id_string, $parent_table ); + } + + // Concatenate each SQL array to generate the final SQL. + $field_sql = implode( ', ', $field_pairs ); + + $from_sql = sprintf( 'FROM %s AS %s', $table_name, $table_alias ); + + $join_sql = implode( ' ', $join_clauses ); + + if ( ! empty( $where_clauses ) ) { + $where_sql = sprintf( 'WHERE %s', implode( ' AND ', $where_clauses ) ); + } + + $group_sql = null; + + if ( ! $parent_table ) { + $group_sql = sprintf( 'GROUP BY %s.id', $table_alias ); + } + + if ( $parent_table ) { + $separator_sql = '|gsmtpfieldsseparator|'; + } + + // Return the resulting SQL + return sprintf( 'JSON_OBJECT( %s ) %s %s %s %s %s %s', $field_sql, $separator_sql, $from_sql, $join_sql, $where_sql, $group_sql, $limit_sql ); + } + + /** + * Categorizes fields as either local (i.e., existing as columns within the table for the object) or meta + * (i.e., existing as custom values in the meta table). + * + * @param Model $object_model + * @param array $fields_to_process + * @param string $table_alias + * + * @return array|array[] + */ + protected function categorize_fields( $object_model, $fields_to_process, $table_alias ) { + $categorized = array( + 'meta' => array(), + 'local' => array(), + ); + + foreach ( $fields_to_process as $field ) { + if ( is_a( $field, Data_Object_From_Array_Token::class ) ) { + $child_type = $field->object_type(); + if ( ! $object_model->relationships()->has( $child_type ) ) { + $error_string = sprintf( 'Attempting to access invalid related object %s for object type %s', $child_type, $object_model->type() ); + throw new \InvalidArgumentException( $error_string, 455 ); + } + + $categorized['local'][ $field->alias() ] = $field; + continue; + } + + $field_name = $field->name(); + + if ( ! array_key_exists( $field_name, $object_model->fields() ) && ! array_key_exists( $field_name, $object_model->meta_fields() ) ) { + $error_string = sprintf( 'Attempting to access invalid field %s on object type %s', $field_name, $object_model->type() ); + throw new \InvalidArgumentException( $error_string, 450 ); + } + + $alias = $field->alias(); + $identifier = $alias ? $alias : $field_name; + + if ( array_key_exists( $field_name, $object_model->fields() ) ) { + $categorized['local'][ $field_name ] = $identifier; + } + + if ( array_key_exists( $field_name, $object_model->meta_fields() ) ) { + $categorized['meta'][ $field_name ] = array( + 'alias' => $identifier, + 'lookup_table_alias' => sprintf( 'meta_%s_%s', $table_alias, $identifier ), + ); + } + } + + return $categorized; + } + + /** + * From the given array of $field_names, build the properly-structured SQL for selecting them. + * + * If a field is detected as a related object, we treat it as a top-level query and recursively + * begin the SQL generation process for it. + * + * @param array $field_names + * @param string $table_alias + * @param string $object_type + * + * @return array + */ + protected function build_local_field_select_clauses( $field_names, $table_alias, $object_type ) { + $pairs = array(); + + foreach ( $field_names as $field_name => $alias ) { + if ( is_a( $alias, Data_Object_From_Array_Token::class ) ) { + $value = '(' . $this->recursively_generate_sql( $alias, $field_name, $table_alias, $object_type ) . ')'; + $pairs[] = sprintf( '"%s", %s', $field_name, $value ); + continue; + } + + $pairs[] = sprintf( '"%s", %s.%s', $alias, $table_alias, $field_name ); + } + + return $pairs; + } + + /** + * Generates the appropriate SQL clause(s) for selecting a Meta field. Meta fields exist in a lookup table, and thus require both a SELECT statement to properly + * query the fields as well as a JOIN clause to join the meta table with a specific alias. + * + * @param string $meta_name The name of the field being selected. + * @param string $alias The alias to use when returning the selected field. + * @param string $object_type The object type to grab the values from. + * @param string $parent_table_alias If present, the parent table this nested query belongs to. + * @param string $idx_prefix If present, the previous IDX prefix used for the parent table. + * (to be prenended to this table's alias) + * + * @return array + */ + protected function build_meta_query( $meta_name, $alias, $object_type, $parent_table_alias, $idx_prefix ) { + global $wpdb; + + $meta_table_name = $wpdb->prefix . $this->db_namespace . '_' . 'meta'; + $meta_table_alias = sprintf( 'meta_%s%s', $meta_name, is_null( $idx_prefix ) ? null : '_' . $idx_prefix ); + + $select_clause = sprintf( + '"%1$s", %2$s.meta_value', + $alias, + $meta_table_alias + ); + + $join_clause = sprintf( + 'LEFT JOIN %1$s AS %2$s ON %3$s.object_id = %4$s.id AND %5$s.object_type = "%6$s" AND %7$s.meta_name = "%8$s"', + $meta_table_name, + $meta_table_alias, + $meta_table_alias, + $parent_table_alias, + $meta_table_alias, + $object_type, + $meta_table_alias, + $meta_name + ); + + return array( + 'select_clause' => $select_clause, + 'join_clause' => $join_clause, + ); + } + + /** + * Build a SQL WHERE clause from the given set of conditions. + * + * @param array $conditions + * + * @return void + */ + protected function build_where_clauses( $conditions ) { + global $wpdb; + + $clauses = array(); + + foreach ( $conditions as $condition ) { + $column_name = $condition['key']; + $column_value = $condition['value']; + $comparator = $condition['comparator']; + + $clauses[] = sprintf( '%1$s %2$s %3$s', $column_name, $comparator, $column_value ); + } + + return $clauses; + } + + /** + * Compose the proper table name for a given object type. + * + * @param string $object_type + * + * @return string + */ + private function compose_table_name( $object_type ) { + global $wpdb; + + return sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $object_type ); + } + + /** + * Compose the appropriate join table name for a given suffix. + * + * @param string $suffix + * + * @return string + */ + private function compose_join_table_name( $suffix ) { + global $wpdb; + + return sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $suffix ); + } + + /** + * Composes a table alias to ensure every table has a unique name in the query. + * + * @param string $object_name + * @param string $parent_table_name + * + * @return string + */ + private function compose_table_alias( $object_name, $parent_table_name = false ) { + if ( ! empty( $parent_table_name ) ) { + return sprintf( '%s_%s', $parent_table_name, $object_name ); + } + + return sprintf( 'table_%s', $object_name ); + } + + /** + * Search an array of arguments and return the appropriate SQL LIMIT clause from + * the values. + * + * @param array $arguments + * + * @return string + */ + private function get_limit_from_arguments( $arguments ) { + $response = ''; + + $limit = array_values( array_filter( $arguments, function ( $item ) { + return $item['key'] === 'limit'; + } ) ); + + $offset = array_values( array_filter( $arguments, function ( $item ) { + return $item['key'] === 'offset'; + } ) ); + + if ( ! empty( $limit ) ) { + $response .= sprintf( 'LIMIT %s', $limit[0]['value'] ); + } + + if ( ! empty( $offset ) ) { + $response .= sprintf( ' OFFSET %s', $offset[0]['value'] ); + } + + return $response; + } + + /** + * Search an array of arguments and compose the appropriate WHERE clause for the values provided. + * + * @param array $where_clauses + * @param string $table_alias + * @param array $arguments + * + * @return void + */ + private function get_where_clauses_from_arguments( &$where_clauses, $table_alias, $arguments ) { + foreach ( $arguments as $argument ) { + if ( $argument['key'] === 'limit' || $argument['key'] === 'offset' ) { + continue; + } + + if ( $argument['comparator'] === 'in' ) { + $in_vals = explode( '|', $argument['value'] ); + foreach ( $in_vals as $key => $value ) { + $in_vals[ $key ] = sprintf( '"%s"', $value ); + } + $in_string = implode( ', ', $in_vals ); + $clause = sprintf( '%s.%s IN (%s)', $table_alias, $argument['key'], $in_string ); + $where_clauses[] = $clause; + continue; + } + + $clause = sprintf( '%s.%s %s "%s"', $table_alias, $argument['key'], $argument['comparator'], $argument['value'] ); + $where_clauses[] = $clause; + } + } + +} \ No newline at end of file diff --git a/src/Utils/class-booliesh.php b/src/Utils/class-booliesh.php new file mode 100644 index 0000000..a2a7fc6 --- /dev/null +++ b/src/Utils/class-booliesh.php @@ -0,0 +1,26 @@ +assertEquals( 'after', $foo ); } - public function testProviderIsRegistered() { - global $fooReg; - $fooReg = 'before'; - $mock = $this->getMockForAbstractClass( Service_Provider::class ); - $mock->expects( $this->any() )->method( 'register' )->willReturnCallback( function() { - global $fooReg; - $fooReg = 'after'; - }); - - $this->assertEquals( 'before', $fooReg ); - - $container = new Service_Container(); - $container->add_provider( $mock ); - - $this->assertEquals( 'after', $fooReg ); - } - - public function testProviderIsInitialized() { - $mock = $this->getMockForAbstractClass( Service_Provider::class, array(), '', true, true, true, array( 'init' ) ); - $mock->expects( $this->once() )->method( 'init' ); - - $container = new Service_Container(); - $container->add_provider( $mock ); - } - } \ No newline at end of file diff --git a/tests/_data/basic.graphql b/tests/_data/basic.graphql new file mode 100644 index 0000000..6cd6c11 --- /dev/null +++ b/tests/_data/basic.graphql @@ -0,0 +1,18 @@ +{ + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } +} \ No newline at end of file diff --git a/tests/_data/chonker.graphql b/tests/_data/chonker.graphql new file mode 100644 index 0000000..00cb6f0 --- /dev/null +++ b/tests/_data/chonker.graphql @@ -0,0 +1,2721 @@ +{ + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } + + hero { + name + metricHeight: height(format: METRIC) + mainFriends: friends { + id(format: INT) + date + secondaryFriends: friends(created_gt: 11/12/2023) { + created_at + website + tertiaryFriends: friends { + email + phone + } + } + } + } +} \ No newline at end of file diff --git a/tests/_data/contact_with_meta.graphql b/tests/_data/contact_with_meta.graphql new file mode 100644 index 0000000..78b35ac --- /dev/null +++ b/tests/_data/contact_with_meta.graphql @@ -0,0 +1,11 @@ +{ + contact { + first_name, + main_email: email, + phone, + phone_two: secondary_phone + friends: contact { + first_name + } + } +} \ No newline at end of file diff --git a/tests/_data/group_to_contact.graphql b/tests/_data/group_to_contact.graphql new file mode 100644 index 0000000..fc760fb --- /dev/null +++ b/tests/_data/group_to_contact.graphql @@ -0,0 +1,18 @@ +{ + group { + name: label, + employees: contact { + first_name, + lname: last_name, + phone_two: secondary_phone + } + }, + company: group { + name: label, + employees: contact { + first_name, + lname: last_name, + phone_two: secondary_phone + } + } +} \ No newline at end of file diff --git a/tests/hermes/HandlerTest.php b/tests/hermes/HandlerTest.php new file mode 100644 index 0000000..264fe29 --- /dev/null +++ b/tests/hermes/HandlerTest.php @@ -0,0 +1,327 @@ +model_collection = new Model_Collection(); + $this->contact_model = new \FakeContactModel(); + $this->group_model = new \FakeGroupModel(); + + $this->model_collection->add( 'contact', $this->contact_model ); + $this->model_collection->add( 'group', $this->group_model ); + + $this->db_namespace = 'gravitycrm'; + + $this->query_handler = new Query_Handler( $this->db_namespace, $this->model_collection ); + + $runners = array( + 'insert' => new Insert_Runner( $this->db_namespace, $this->query_handler ), + 'delete' => new Delete_Runner( $this->db_namespace, $this->query_handler ), + 'connect' => new Connect_Runner( $this->db_namespace, $this->query_handler ), + 'update' => new Update_Runner( $this->db_namespace, $this->query_handler ), + ); + + $this->mutation_handler = new Mutation_Handler( $this->db_namespace, $this->model_collection, $this->query_handler, $runners ); + } + + /** + * @dataProvider mutationHandlerProvider + * + * @param $text + * @param $expected + * + * @return void + */ + public function testMutationHandler( $text, $expected ) { + + try { + $data = $this->mutation_handler->handle_mutation( $text ); + } catch ( \Exception $e ) { + $this->assertEquals( $expected, 'failure' ); + + return; + } + + $this->assertEquals( $expected, 'success' ); + } + + public function mutationHandlerProvider() { + return array( + // Valid + array( + '{ + insert_contact(objects: [{email: "foo@bar.com", first_name: "Foo", last_name: "Bar"}, {first_name: "Bing", last_name: "Bash", secondary_phone: "4445554848" }]) { + returning { + id, + first_name, + last_name, + secondary_phone, + } + } +}', + 'success', + ), + + // Invalid email field + array( + '{ + insert_contact(objects: [{email: "foo@bar", first_name: "Foo", last_name: "Bar"}, {first_name: "Bing", last_name: "Bash", secondary_phone: "4445554848" }]) { + returning { + id, + first_name, + last_name, + secondary_phone, + } + } +}', + 'failure', + ), + + // Invalid custom callback + array( + '{ + insert_contact(objects: [{foobar: "bar", first_name: true, last_name: "Bar"}, {first_name: "Bing", last_name: "Bash", secondary_phone: "4445554848" }]) { + returning { + id, + first_name, + last_name, + secondary_phone, + } + } +}', + 'failure', + ), + + // Valid update + array( + '{ + update_contact(id: 1, email: "foo@bar.com", first_name: "Foo", last_name: "Bar", secondary_phone: "4445554848") { + returning { + id, + first_name, + last_name, + secondary_phone, + } + } +}', + 'success', + ), + + // Update missing ID + array( + '{ + update_contact( email: "foo@bar.com", first_name: "Foo", last_name: "Bar", secondary_phone: "4445554848") { + returning { + id, + first_name, + last_name, + secondary_phone, + } + } +}', + 'failure', + ), + + // Delete + array( + '{ + delete_contact(id: 1) { + } +}', + 'success', + ), + + // Delete missing ID + array( + '{ + delete_contact() { + } +}', + 'failure', + ), + + // Delete missing valid object type + array( + '{ + delete_invalid_object(id: 1) { + } +}', + 'failure', + ), + + // Connect + array( + '{ + connect_group_contact(from: 1, to: 2) { + } +}', + 'success', + ), + ); + } + + public function testInsertMutation() { + global $wpdb; + \gravitytools_tests_reset_db(); + + $text = '{ + insert_contact(objects: [{email: "foo@bar.com", first_name: "Foo", last_name: "Bar"}, {first_name: "Bing", last_name: "Bash", secondary_phone: "4445554848" }]) { + returning { + id, + first_name, + last_name, + secondary_phone, + } + } +}'; + + $this->mutation_handler->handle_mutation( $text ); + $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'contact' ); + $meta_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'meta' ); + + $check_query = sprintf( 'SELECT * FROM %s', $table_name ); + $results = $wpdb->get_results( $check_query, ARRAY_A ); + + $this->assertEquals( 2, count( $results ) ); + + $record = $results[1]; + + $this->assertEquals( 'Bing', $record['first_name'] ); + + $meta_check_query = sprintf( 'SELECT meta_value FROM %s WHERE object_id = "%s" AND meta_name = "%s"', $meta_table_name, $record['id'], 'secondary_phone' ); + $meta_results = $wpdb->get_results( $meta_check_query, ARRAY_A ); + + $this->assertEquals( '4445554848', $meta_results[0]['meta_value'] ); + } + + public function testUpdateMutation() { + global $wpdb; + \gravitytools_tests_reset_db(); + + $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'contact' ); + $meta_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'meta' ); + + $insert_query = sprintf( 'INSERT INTO %s (first_name, last_name, email, phone) VALUES ("Test", "User", "test@gravity.local", "5556665656" )', $table_name ); + $wpdb->query( $insert_query ); + + $insert_meta_query = sprintf( 'INSERT INTO %s (meta_name, meta_value, object_type, object_id) VALUES ("secondary_phone", "4445554545", "contact", "1" )', $meta_table_name ); + $wpdb->query( $insert_meta_query ); + + $text = '{ + update_contact(id: 1, email: "foo@bar.com", first_name: "Foo", last_name: "Bar", secondary_phone: "4445554848") { + returning { + id, + first_name, + last_name, + secondary_phone, + } + } +}'; + + $this->mutation_handler->handle_mutation( $text ); + + $check_query = sprintf( 'SELECT * FROM %s', $table_name ); + $results = $wpdb->get_results( $check_query, ARRAY_A ); + + $this->assertEquals( 1, count( $results ) ); + + $check_query = sprintf( 'SELECT * FROM %s WHERE id = "%s"', $table_name, 1 ); + $results = $wpdb->get_results( $check_query, ARRAY_A ); + + $record = $results[0]; + + $this->assertEquals( 'Foo', $record['first_name'] ); + $this->assertEquals( 'Bar', $record['last_name'] ); + $this->assertEquals( 'foo@bar.com', $record['email'] ); + + $meta_check_query = sprintf( 'SELECT meta_value FROM %s WHERE object_id = "%s" AND meta_name = "%s"', $meta_table_name, $record['id'], 'secondary_phone' ); + $meta_results = $wpdb->get_results( $meta_check_query, ARRAY_A ); + + $this->assertEquals( '4445554848', $meta_results[0]['meta_value'] ); + + } + + public function testDeleteMutation() { + global $wpdb; + \gravitytools_tests_reset_db(); + + $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'contact' ); + $meta_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'meta' ); + + $insert_query = sprintf( 'INSERT INTO %s (first_name, last_name, email, phone) VALUES ("Test", "User", "test@gravity.local", "5556665656" )', $table_name ); + $wpdb->query( $insert_query ); + + $text = '{ + delete_contact(id: 1) {} +}'; + + $check_query = sprintf( 'SELECT * FROM %s', $table_name ); + $results = $wpdb->get_results( $check_query, ARRAY_A ); + + $this->assertEquals( 1, count( $results ) ); + + $this->mutation_handler->handle_mutation( $text ); + + $check_query = sprintf( 'SELECT * FROM %s', $table_name ); + $results = $wpdb->get_results( $check_query, ARRAY_A ); + + $this->assertEquals( 0, count( $results ) ); + } + + public function testConnectMutation() { + global $wpdb; + \gravitytools_tests_reset_db(); + + $contact_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'contact' ); + $group_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'group' ); + $connect_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'group_contact' ); + + $insert_query = sprintf( 'INSERT INTO %s (first_name, last_name, email, phone) VALUES ("Test", "User", "test@gravity.local", "5556665656" )', $contact_table_name ); + $wpdb->query( $insert_query ); + + $insert_query = sprintf( 'INSERT INTO %s (label) VALUES ("Test Group")', $group_table_name ); + $wpdb->query( $insert_query ); + + $text = '{ + connect_group_contact(from: 1, to: 1) {} +}'; + + $check_query = sprintf( 'SELECT * FROM %s', $connect_table_name ); + $results = $wpdb->get_results( $check_query, ARRAY_A ); + + $this->assertEquals( 0, count( $results ) ); + + $this->mutation_handler->handle_mutation( $text ); + + $check_query = sprintf( 'SELECT * FROM %s', $connect_table_name ); + $results = $wpdb->get_results( $check_query, ARRAY_A ); + + $this->assertEquals( 1, count( $results ) ); + $this->assertEquals( 1, $results[0]['group_id'] ); + $this->assertEquals( 1, $results[0]['contact_id'] ); + } + +} diff --git a/tests/hermes/enum/FieldTypeValidationEnumTest.php b/tests/hermes/enum/FieldTypeValidationEnumTest.php new file mode 100644 index 0000000..8f762d5 --- /dev/null +++ b/tests/hermes/enum/FieldTypeValidationEnumTest.php @@ -0,0 +1,327 @@ +assertEquals( $expected, $result ); + } + + + /** + * Returns an array with the following values: + * + * - Field type to check + * - Value to validate + * - Expected result + * + * + * @return array[] + */ + public function fieldValuesProvider() { + return array( + + // Basic string + array( + Field_Type_Validation_Enum::STRING, + 'foobar', + 'foobar' + ), + + // String that needs to be slashified + array( + Field_Type_Validation_Enum::STRING, + "'s-Hertogenbosch", + "\'s-Hertogenbosch", + ), + + // Non-string that should return null + array( + Field_Type_Validation_Enum::STRING, + array(), + null, + ), + + // Non-string that should return null + array( + Field_Type_Validation_Enum::STRING, + 12, + null, + ), + + // Basic integer + array( + Field_Type_Validation_Enum::INT, + 5, + 5, + ), + + // Basic string integer + array( + Field_Type_Validation_Enum::INT, + '5', + 5, + ), + + // Float cast as integer + array( + Field_Type_Validation_Enum::INT, + 5.0, + 5, + ), + + // Non-numeric string that should return null + array( + Field_Type_Validation_Enum::INT, + 'five', + null, + ), + + // Non-integer that should return null + array( + Field_Type_Validation_Enum::INT, + array(), + null, + ), + + // Basic float + array( + Field_Type_Validation_Enum::FLOAT, + 5.25, + 5.25 + ), + + // String masquerading as float + array( + Field_Type_Validation_Enum::FLOAT, + '5.25', + 5.25 + ), + + // Invalid float + array( + Field_Type_Validation_Enum::FLOAT, + '5.2f', + null + ), + + // Truly unhinged float + array( + Field_Type_Validation_Enum::FLOAT, + array( 'FLOAT' ), + null + ), + + // Boolean true + array( + Field_Type_Validation_Enum::BOOLEAN, + true, + true + ), + + // Boolean true + array( + Field_Type_Validation_Enum::BOOLEAN, + 'true', + true + ), + + // Boolean true + array( + Field_Type_Validation_Enum::BOOLEAN, + 1, + true + ), + + // Boolean true + array( + Field_Type_Validation_Enum::BOOLEAN, + 'yes', + true + ), + + // Boolean true + array( + Field_Type_Validation_Enum::BOOLEAN, + '1', + true + ), + + // Boolean true + array( + Field_Type_Validation_Enum::BOOLEAN, + 'on', + true + ), + + // Boolean false + array( + Field_Type_Validation_Enum::BOOLEAN, + false, + false + ), + + // Boolean false + array( + Field_Type_Validation_Enum::BOOLEAN, + 'false', + false + ), + + // Boolean false + array( + Field_Type_Validation_Enum::BOOLEAN, + 0, + false + ), + + // Boolean false + array( + Field_Type_Validation_Enum::BOOLEAN, + 'no', + false + ), + + // Boolean false + array( + Field_Type_Validation_Enum::BOOLEAN, + '0', + false + ), + + // Boolean false + array( + Field_Type_Validation_Enum::BOOLEAN, + 'off', + false + ), + + // Boolean null + array( + Field_Type_Validation_Enum::BOOLEAN, + new \stdClass(), + null + ), + + // Y/m/d + array( + Field_Type_Validation_Enum::DATE, + '2000/12/01', + '2000/12/01' + ), + + // m-d-Y + array( + Field_Type_Validation_Enum::DATE, + '12-01-2000', + '12-01-2000' + ), + + // Timestamp + array( + Field_Type_Validation_Enum::DATE, + '2000-12-01 12:00:00', + '2000-12-01 12:00:00' + ), + + // Invalid m-d-y + array( + Field_Type_Validation_Enum::DATE, + '24-24-2000', + null + ), + + // Not a date + array( + Field_Type_Validation_Enum::DATE, + array(), + null + ), + + // Simple email + array( + Field_Type_Validation_Enum::EMAIL, + 'foo@bar.com', + 'foo@bar.com', + ), + + // advanced email + array( + Field_Type_Validation_Enum::EMAIL, + 'foo+stu_ff.here@bar.com', + 'foo+stu_ff.here@bar.com', + ), + + // Bad email + array( + Field_Type_Validation_Enum::EMAIL, + 'foo@bar', + null, + ), + + // Another bad email + array( + Field_Type_Validation_Enum::EMAIL, + 'foo here@bar.com', + null, + ), + + // Custom Callback + array( + function( $value ) { + return 'hey'; + }, + 'foo here@bar.com', + 'hey', + ), + + // Custom array callback + array( + array( $this, 'custom_validation_callback' ), + 'foo', + 'got foo', + ), + + // Custom array callback + array( + array( $this, 'custom_validation_callback' ), + 'bar', + 'got bar', + ), + + // Custom array callback + array( + array( $this, 'custom_validation_callback' ), + 'bazinga', + null, + ), + ); + } + + public function custom_validation_callback( $value ) { + if ( $value === 'foo' ) { + return 'got foo'; + } + + if ( $value === 'bar' ) { + return 'got bar'; + } + + return null; + } + +} \ No newline at end of file diff --git a/tests/hermes/tokens/ArgumentsTokenTest.php b/tests/hermes/tokens/ArgumentsTokenTest.php new file mode 100644 index 0000000..27b5683 --- /dev/null +++ b/tests/hermes/tokens/ArgumentsTokenTest.php @@ -0,0 +1,155 @@ +assertEquals( $expected, $token->items() ); + } + + public function queryStringProvider() { + return array( + + // Single argument with standard equal operator + array( + 'id: 5', + array( + array( + 'key' => 'id', + 'comparator' => '=', + 'value' => '5', + ) + ) + ), + + // Multiple arguments with equal operators + array( + 'id: 5, date: 2000-01-01', + array( + array( + 'key' => 'id', + 'comparator' => '=', + 'value' => '5', + ), + array( + 'key' => 'date', + 'comparator' => '=', + 'value' => '2000-01-01', + ) + ) + ), + + // Single argument with lt operator + array( + 'id_lt: 5', + array( + array( + 'key' => 'id', + 'comparator' => '<', + 'value' => '5', + ) + ) + ), + + // Single argument with gt operator + array( + 'id_gt: 5', + array( + array( + 'key' => 'id', + 'comparator' => '>', + 'value' => '5', + ) + ) + ), + + // Single argument with lte operator + array( + 'id_lte: 5', + array( + array( + 'key' => 'id', + 'comparator' => '<=', + 'value' => '5', + ) + ) + ), + + // Single argument with gte operator + array( + 'id_gte: 5', + array( + array( + 'key' => 'id', + 'comparator' => '>=', + 'value' => '5', + ) + ) + ), + + // Single argument with ne operator + array( + 'id_ne: 5', + array( + array( + 'key' => 'id', + 'comparator' => '!=', + 'value' => '5', + ) + ) + ), + + // Multiple arguments with mixed operators + array( + 'id: 5, foo_lt: 10, bar_gt: 20, bash_lte: 30, bing_gte: 40, bazinga_ne: 50', + array( + array( + 'key' => 'id', + 'comparator' => '=', + 'value' => '5', + ), + array( + 'key' => 'foo', + 'comparator' => '<', + 'value' => '10', + ), + array( + 'key' => 'bar', + 'comparator' => '>', + 'value' => '20', + ), + array( + 'key' => 'bash', + 'comparator' => '<=', + 'value' => '30', + ), + array( + 'key' => 'bing', + 'comparator' => '>=', + 'value' => '40', + ), + array( + 'key' => 'bazinga', + 'comparator' => '!=', + 'value' => '50', + ), + ), + ), + ); + } + +} \ No newline at end of file diff --git a/tests/hermes/tokens/FieldTokenTest.php b/tests/hermes/tokens/FieldTokenTest.php new file mode 100644 index 0000000..c4b1ff4 --- /dev/null +++ b/tests/hermes/tokens/FieldTokenTest.php @@ -0,0 +1,92 @@ +assertEquals( $expected_name, $token->name() ); + $this->assertEquals( $expected_alias, $token->alias() ); + $this->assertEquals( $expected_arguments, $token->children() ); + } + + public function arrayArgsProvider() { + return array( + array( + array(), + array(), + array( + 'name' => 'foo', + 'alias' => 'foo', + 'arguments' => array(), + ), + 'foo', + 'foo', + array(), + ), + + array( + array(), + array(), + array( + 'name' => 'foo', + 'alias' => 'bash', + 'arguments' => array(), + ), + 'foo', + 'bash', + array(), + ), + + array( + array(), + array(), + array( + 'name' => 'foo', + 'alias' => 'bash', + 'arguments' => array( + 'foo' => 'bar', + ), + ), + 'foo', + 'bash', + array( + 'foo' => 'bar', + ), + ), + + array( + array( 'foo' => 'bar' ), + array( 'bing' => 'bash' ), + array( + 'name' => 'foo', + 'alias' => 'foo', + 'arguments' => array(), + ), + 'foo', + 'foo', + array(), + ) + ); + } + +} \ No newline at end of file diff --git a/tests/hermes/tokens/QueryTokenTest.php b/tests/hermes/tokens/QueryTokenTest.php new file mode 100644 index 0000000..a7a2c01 --- /dev/null +++ b/tests/hermes/tokens/QueryTokenTest.php @@ -0,0 +1,34 @@ +items(); + $this->assertCount( 1, $items ); + + $heroFields = $items[0]->fields(); + $this->assertCount( 3, $heroFields ); + + $friendsFields = $heroFields[2]->fields(); + $this->assertCount( 3, $friendsFields ); + + $secondaryFriendFields = $friendsFields[2]->fields(); + $this->assertCount( 3, $secondaryFriendFields ); + + $tertiaryFriendFields = $secondaryFriendFields[2]->fields(); + $this->assertcount( 2, $tertiaryFriendFields ); + + $this->assertEquals( $heroFields[1]->object_type(), 'height' ); + $this->assertEquals( $heroFields[1]->alias(), 'metricHeight' ); + $this->assertCount( 1, $heroFields[1]->arguments()->items() ); + } + +} \ No newline at end of file diff --git a/tests/hermes/tokens/mutations/InsertMutationTokenTest.php b/tests/hermes/tokens/mutations/InsertMutationTokenTest.php new file mode 100644 index 0000000..5f3fbb9 --- /dev/null +++ b/tests/hermes/tokens/mutations/InsertMutationTokenTest.php @@ -0,0 +1,35 @@ +assertEquals( $expected_fields, $token->return_fields() ); + } + +} \ No newline at end of file diff --git a/wp-loader.php b/wp-loader.php new file mode 100644 index 0000000..db07dc1 --- /dev/null +++ b/wp-loader.php @@ -0,0 +1,349 @@ +setConstants(); + $this->preLoadWP(); + $this->loadWP(); + $this->postLoadWP(); + $this->requireTestCaseParents(); + $this->bootstrapMockAddon(); + $this->onShutdown(); + } + + protected function setConstants() { + if ( ! defined( 'GMT_TESTS_DIR' ) ) { + define( 'GMT_PLUGIN_DIR', dirname( __FILE__ ) . '/' ); + define( 'GMT_TESTS_DIR', GMT_PLUGIN_DIR . 'tests' ); + define( 'WP_TESTS_DIR', GMT_PLUGIN_DIR . 'vendor/wordpress/wordpress/tests/phpunit/' ); + define( 'WP_TESTS_CONFIG_FILE_PATH', GMT_PLUGIN_DIR . '/wp-tests-config.php' ); + } + } + + + protected function preLoadWP() { + //if WordPress test suite isn't found then we can't do anything. + if ( ! is_readable( WP_TESTS_DIR . 'includes/functions.php' ) ) { + die( "The WordPress PHPUnit test suite could not be found at: " . WP_TESTS_DIR ); + } + require_once WP_TESTS_DIR . 'includes/functions.php'; + //set filter for bootstrapping EE which needs to happen BEFORE loading WP. + tests_add_filter( 'muplugins_loaded', array( $this, 'setupAndLoadWP' ) ); + } + + + protected function loadWP() { + FunctionMocker::setup(); + + require WP_TESTS_DIR . 'includes/bootstrap.php'; + + FunctionMocker::replace('wp_send_json_success', '' ); + FunctionMocker::replace('wp_send_json_error', '' ); + } + + + public function setupAndLoadWP() { + if ( ! defined( 'SAVEQUERIES' ) ) { + define( 'SAVEQUERIES', true ); + } + } + + + public function postLoadWP() { + // ensure date and time formats are set + if ( ! get_option( 'date_format' ) ) { + update_option( 'date_format', 'F j, Y' ); + } + if ( ! get_option( 'time_format' ) ) { + update_option( 'time_format', 'g:i a' ); + } + + wp_set_current_user(1); + } + + + protected function requireTestCaseParents() { + // good place to require any other files needed by tests, like mock files and test case parent files + } + + + protected function bootstrapMockAddon() { + // good place to load any add-on files which we might want to also test + } + + + protected function onShutdown() { + //nuke all PMB data once the tests are done, so that it doesn't carry over to the next time we run tests + register_shutdown_function( + function () { + FunctionMocker::tearDown(); + + \gravitytools_tests_reset_db(); + } + ); + } + + //////// Entities //////// + + public function contact() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_contact'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + first_name varchar(100) NOT NULL, + last_name varchar(100) NOT NULL, + email varchar(100) NOT NULL, + phone varchar(100) NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_updated datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + FULLTEXT(first_name), + FULLTEXT(last_name), + FULLTEXT(email), + FULLTEXT(phone), + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + public function company() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_company'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + company_name varchar(100) NOT NULL, + url varchar(100) NOT NULL, + description text NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_updated datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + FULLTEXT(company_name), + FULLTEXT(url), + FULLTEXT(description), + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + public function group() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_group'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + label varchar(100) NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_updated datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + FULLTEXT(label), + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + public function deal() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_deal'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + label varchar(100) NOT NULL, + status varchar(100) NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_updated datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + FULLTEXT(label), + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + public function pipeline() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_pipeline'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + label varchar(100) NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_updated datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + FULLTEXT(label), + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + //////// Relationships //////// + + public function company_to_contact() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_company_contact'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + company_id mediumint(9) NOT NULL, + contact_id mediumint(9) NOT NULL, + is_main tinyint(1) DEFAULT 0 NOT NULL, + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + public function group_to_contact() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_group_contact'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + group_id mediumint(9) NOT NULL, + contact_id mediumint(9) NOT NULL, + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + public function deal_to_company() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_deal_company'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + deal_id mediumint(9) NOT NULL, + company_id mediumint(9) NOT NULL, + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + public function deal_to_contact() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_deal_contact'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + deal_id mediumint(9) NOT NULL, + contact_id mediumint(9) NOT NULL, + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + public function pipeline_to_deal() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_pipeline_deal'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + pipeline_id mediumint(9) NOT NULL, + deal_id mediumint(9) NOT NULL, + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } + + //////// Meta //////// + + public function meta() { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $table_name = $wpdb->prefix . 'gravitycrm_meta'; + + $sql = " + CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + object_type varchar(100) NOT NULL, + object_id mediumint(9) NOT NULL, + meta_name varchar(100) NOT NULL, + meta_value mediumtext NOT NULL, + FULLTEXT(meta_value), + PRIMARY KEY (id) + ) $charset_collate; + "; + + dbDelta( $sql ); + } +} \ No newline at end of file