├── README.md ├── tests ├── Admin │ ├── Media │ │ ├── upload.gif │ │ └── StandardTest.php │ ├── Coupon │ │ └── StandardTest.php │ ├── Plugin │ │ └── StandardTest.php │ ├── Rule │ │ └── StandardTest.php │ ├── Customer │ │ └── StandardTest.php │ ├── Service │ │ └── StandardTest.php │ ├── Index │ │ └── StandardTest.php │ ├── StandardTest.php │ ├── Order │ │ └── StandardTest.php │ ├── Locale │ │ └── Site │ │ │ └── StandardTest.php │ ├── Product │ │ └── StandardTest.php │ └── Catalog │ │ └── StandardTest.php ├── phpunit.xml ├── bootstrap.php ├── phpunit-coverage.xml └── TestHelper.php ├── .gitignore ├── manifest.php ├── src ├── Admin │ ├── Graphql │ │ ├── Exception.php │ │ ├── Rule │ │ │ └── Standard.php │ │ ├── Plugin │ │ │ └── Standard.php │ │ ├── Supplier │ │ │ └── Standard.php │ │ ├── Attribute │ │ │ └── Standard.php │ │ ├── Coupon │ │ │ └── Standard.php │ │ ├── ProviderTrait.php │ │ ├── Product │ │ │ └── Standard.php │ │ ├── Service │ │ │ └── Standard.php │ │ ├── Index │ │ │ └── Standard.php │ │ ├── Standard.php │ │ ├── Media │ │ │ └── Standard.php │ │ ├── Customer │ │ │ └── Standard.php │ │ ├── UpdateTrait.php │ │ ├── Base.php │ │ ├── Catalog │ │ │ └── Standard.php │ │ ├── Locale │ │ │ └── Site │ │ │ │ └── Standard.php │ │ └── Order │ │ │ └── Standard.php │ └── Graphql.php └── GraphQL │ └── Type │ └── Definition │ ├── Json.php │ └── Upload.php ├── composer.json ├── i18n └── controller │ ├── id.po │ ├── bn.po │ ├── da.po │ ├── el.po │ ├── gu.po │ ├── hy.po │ ├── az.po │ ├── en_US.po │ ├── et.po │ ├── fr_LU.po │ ├── ar.po │ ├── hi.po │ ├── he.po │ ├── fa.po │ ├── fi.po │ ├── cs.po │ ├── bg.po │ ├── es.po │ ├── de.po │ ├── hr.po │ ├── hu.po │ └── fr.po ├── phing.xml ├── config └── admin.php └── .circleci └── config.yml /README.md: -------------------------------------------------------------------------------- 1 | # ai-admin-graphql 2 | Aimeos GraphQL API admin interface 3 | -------------------------------------------------------------------------------- /tests/Admin/Media/upload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aimeos/ai-admin-graphql/HEAD/tests/Admin/Media/upload.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.result.cache 2 | .phpunit.cache 3 | coveralls.json 4 | coverage.xml 5 | *.log 6 | *.ser 7 | tests/tmp 8 | -------------------------------------------------------------------------------- /manifest.php: -------------------------------------------------------------------------------- 1 | 'ai-admin-graphql', 5 | 'config' => [ 6 | 'config', 7 | ], 8 | 'depends' => [ 9 | 'aimeos-core', 10 | ], 11 | 'include' => [ 12 | 'src', 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./ 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Exception.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ../src 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Admin/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aimeos/ai-admin-graphql", 3 | "description": "Aimeos Admin GraphQL API extension", 4 | "keywords": ["aimeos", "extension", "graphql"], 5 | "type": "aimeos-extension", 6 | "license": "LGPL-3.0", 7 | "support": { 8 | "source": "https://github.com/aimeos/ai-admin-graphql", 9 | "issues": "https://github.com/aimeos/ai-admin-graphql/issues", 10 | "forum": "https://aimeos.org/help", 11 | "wiki": "https://aimeos.org/docs" 12 | }, 13 | "prefer-stable": true, 14 | "minimum-stability": "dev", 15 | "require": { 16 | "php": "^8.0.11", 17 | "aimeos/aimeos-core": "dev-master", 18 | "webonyx/graphql-php": "~15.0", 19 | "nyholm/psr7": "~1.2" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "~10.0||~11.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Aimeos\\": "src" 27 | }, 28 | "classmap": [ 29 | "src" 30 | ] 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Aimeos\\": "tests" 35 | }, 36 | "classmap": [ 37 | "tests" 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /tests/Admin/Coupon/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testGetCouponConfig() 27 | { 28 | $body = '{"query":"query {\n getCouponConfig(provider: \"Voucher,Basket\") {\n code\n type\n label\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"code":"voucher.productcode"', (string) $response->getBody() ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Admin/Plugin/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testGetPluginConfig() 27 | { 28 | $body = '{"query":"query {\n getPluginConfig(provider: \"Autofill,Log\", type: \"order\") {\n code\n type\n label\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"code":"useorder"', (string) $response->getBody() ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Admin/Rule/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testGetRuleConfig() 27 | { 28 | $body = '{"query":"query {\n getRuleConfig(provider: \"Percent,Category\", type: \"catalog\") {\n code\n type\n label\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"code":"percent"', (string) $response->getBody() ); 34 | $this->assertStringContainsString( '"code":"category.code"', (string) $response->getBody() ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Supplier/Standard.php: -------------------------------------------------------------------------------- 1 | $this->types()->outputType( $domain ), 36 | 'args' => [ 37 | ['name' => 'code', 'type' => Type::string(), 'description' => 'Unique code'], 38 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 39 | ], 40 | 'resolve' => $this->findItem( $domain ), 41 | ]; 42 | 43 | return $list; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Admin/Customer/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testFindCustomer() 27 | { 28 | $body = '{"query":"query {\n findCustomer(code: \"test@example.com\") {\n id\n code groups\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"code":"test@example.com"', (string) $response->getBody() ); 34 | } 35 | 36 | 37 | public function testAggregateCustomers() 38 | { 39 | $body = '{"query":"query {\n aggregateCustomers(key: [\"customer.status\"]) {\n aggregates\n }\n}\n","variables":{},"operationName":null}'; 40 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 41 | 42 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 43 | 44 | $this->assertStringContainsString( '{\"0\":1,\"1\":2}', (string) $response->getBody() ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /i18n/controller/id.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Indonesian (http://app.transifex.com/aimeos/aimeos-core/language/id/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: id\n" 18 | "Plural-Forms: nplurals=1; plural=0;\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/bn.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Bengali (http://app.transifex.com/aimeos/aimeos-core/language/bn/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: bn\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/da.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Danish (http://app.transifex.com/aimeos/aimeos-core/language/da/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: da\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/el.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Greek (http://app.transifex.com/aimeos/aimeos-core/language/el/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: el\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/gu.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Gujarati (http://app.transifex.com/aimeos/aimeos-core/language/gu/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: gu\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/hy.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Armenian (http://app.transifex.com/aimeos/aimeos-core/language/hy/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: hy\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/az.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Azerbaijani (http://app.transifex.com/aimeos/aimeos-core/language/az/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: az\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/en_US.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: English (United States) (http://app.transifex.com/aimeos/aimeos-core/language/en_US/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: en_US\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/et.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Estonian (http://app.transifex.com/aimeos/aimeos-core/language/et/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: et\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "Kategooriast \"%3$s\" ei leitud ühtegi toodet nimega \"%1$s/%2$s\"" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /tests/Admin/Service/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testFindService() 27 | { 28 | $body = '{"query":"query {\n findService(code: \"unitpaymentcode\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"code":"unitpaymentcode"', (string) $response->getBody() ); 34 | } 35 | 36 | 37 | public function testGetServiceConfig() 38 | { 39 | $body = '{"query":"query {\n getServiceConfig(provider: \"Xml,BasketValues\", type: \"delivery\") {\n code\n type\n label\n }\n}\n","variables":{},"operationName":null}'; 40 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 41 | 42 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 43 | 44 | $this->assertStringContainsString( '"code":"xml.exportpath"', (string) $response->getBody() ); 45 | $this->assertStringContainsString( '"code":"basketvalues.total-value-min"', (string) $response->getBody() ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /i18n/controller/fr_LU.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: French (Luxembourg) (http://app.transifex.com/aimeos/aimeos-core/language/fr_LU/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: fr_LU\n" 18 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/ar.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Arabic (http://app.transifex.com/aimeos/aimeos-core/language/ar/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: ar\n" 18 | "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/hi.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Dharit Maniyar, 2022 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: aimeos-core\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 12 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 13 | "Last-Translator: Dharit Maniyar, 2022\n" 14 | "Language-Team: Hindi (http://app.transifex.com/aimeos/aimeos-core/language/hi/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: hi\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #, php-format 22 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 23 | msgstr "" 24 | 25 | #, php-format 26 | msgid "Class \"%1$s\" not available" 27 | msgstr "" 28 | 29 | #, php-format 30 | msgid "Class \"%1$s\" not found" 31 | msgstr "" 32 | 33 | #, php-format 34 | msgid "Downloading file \"%1$s\" failed" 35 | msgstr "फ़ाइल \"%1$s\" डाउनलोड करना विफल रहा" 36 | 37 | #, php-format 38 | msgid "File \"%1$s\" not found" 39 | msgstr "फाइल \"%1$s \" नहीं मिली" 40 | 41 | #, php-format 42 | msgid "File type \"%1$s\" is not allowed" 43 | msgstr "" 44 | 45 | #, php-format 46 | msgid "Invalid characters in class name \"%1$s\"" 47 | msgstr "" 48 | 49 | #, php-format 50 | msgid "No allowed image types configured for \"%1$s\"" 51 | msgstr "" 52 | 53 | #, php-format 54 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 55 | msgstr "" 56 | 57 | #, php-format 58 | msgid "Unable to create directory \"%1$s\"" 59 | msgstr "" 60 | 61 | #, php-format 62 | msgid "Unable to create file in \"%1$s\"" 63 | msgstr "" 64 | -------------------------------------------------------------------------------- /i18n/controller/he.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-26 15:47+0200\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Hebrew (http://app.transifex.com/aimeos/aimeos-core/language/he/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: he\n" 18 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/fa.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Persian (http://app.transifex.com/aimeos/aimeos-core/language/fa/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: fa\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "کلاس \"%1$s\" از رابط کاربری \"%2$s\" پیاده سازی نشده است" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "کلاس \"%1$s\" معتبر نمی‌باشد" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "کلاس \"%1$s\" موجود نمی‌باشد" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "کاراکترهای نا معتبر در کلاس \"%1$s\"" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "هیچ نوع آیتمی برای \"%1$s/%2$s\" در \"%3$s\" پیدا نشد" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /i18n/controller/fi.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: aimeos-core\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 11 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Finnish (http://app.transifex.com/aimeos/aimeos-core/language/fi/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: fi\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #, php-format 21 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 22 | msgstr "Luokka \"%1$s\" ei toteuta liitäntää \"%2$s\"" 23 | 24 | #, php-format 25 | msgid "Class \"%1$s\" not available" 26 | msgstr "Luokka \"%1$s\" ei saatavilla" 27 | 28 | #, php-format 29 | msgid "Class \"%1$s\" not found" 30 | msgstr "Luokka \"%1$s\" ei löydy" 31 | 32 | #, php-format 33 | msgid "Downloading file \"%1$s\" failed" 34 | msgstr "" 35 | 36 | #, php-format 37 | msgid "File \"%1$s\" not found" 38 | msgstr "" 39 | 40 | #, php-format 41 | msgid "File type \"%1$s\" is not allowed" 42 | msgstr "" 43 | 44 | #, php-format 45 | msgid "Invalid characters in class name \"%1$s\"" 46 | msgstr "Virheellisiä merkkejä luokkanimessä \"%1$s\"" 47 | 48 | #, php-format 49 | msgid "No allowed image types configured for \"%1$s\"" 50 | msgstr "" 51 | 52 | #, php-format 53 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 54 | msgstr "Minkään tyyppistä tavaraa ei löytynyt osiosta \"%3$s” haulla \"%1$s/%2$s\"" 55 | 56 | #, php-format 57 | msgid "Unable to create directory \"%1$s\"" 58 | msgstr "" 59 | 60 | #, php-format 61 | msgid "Unable to create file in \"%1$s\"" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /phing.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Attribute/Standard.php: -------------------------------------------------------------------------------- 1 | $this->types()->outputType( $domain ), 36 | 'args' => [ 37 | ['name' => 'code', 'type' => Type::string(), 'description' => 'Unique code'], 38 | ['name' => 'domain', 'type' => Type::string(), 'description' => 'Domain of the attribute'], 39 | ['name' => 'type', 'type' => Type::string(), 'description' => 'Attribute type'], 40 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 41 | ], 42 | 'resolve' => $this->findItem( $domain ), 43 | ]; 44 | 45 | return $list; 46 | } 47 | 48 | 49 | /** 50 | * Returns a closure for returning a single item by its code 51 | * 52 | * @param string $domain Domain path of the manager 53 | * @return \Closure Anonymous method returning one item 54 | */ 55 | protected function findItem( string $domain ) : \Closure 56 | { 57 | return function( $root, $args, $context ) use ( $domain ) { 58 | return \Aimeos\MShop::create( $this->context(), $domain )->find( $args['code'], $args['include'], $args['domain'], $args['type'] ); 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Admin/Media/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testSaveMedia() 27 | { 28 | $content = file_get_contents( __DIR__ . '/upload.gif' ); 29 | $stream = \Nyholm\Psr7\Stream::create( $content ); 30 | $file = new \Nyholm\Psr7\UploadedFile( $stream, strlen( $content ), \UPLOAD_ERR_OK, 'upload.gif' ); 31 | 32 | $stub = $this->getMockBuilder( '\\Aimeos\\MShop\\Media\\Manager\\Standard' ) 33 | ->setConstructorArgs( [$this->context] ) 34 | ->onlyMethods( ['save', 'type'] ) 35 | ->getMock(); 36 | 37 | $stub->method( 'type' )->willReturn( ['media'] ); 38 | $stub->expects( $this->once() )->method( 'save' )->willReturnArgument( 0 ); 39 | 40 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Media\\Manager\\Standard', $stub ); 41 | 42 | $body = '{"query":"mutation($file: Upload, $preview: Upload) {\n saveMedia(input: {\n domain: \"product\"\n file: $file\n filepreview: $preview\n }) {\n id\n label\n url\n preview\n }\n}\n","variables":{ "file": null, "preview": null },"operationName":null}'; 43 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost' ); 44 | $request = $request->withParsedBody( ['operations' => $body, 'map' => json_encode( [1 => ['variables.file']] )] ); 45 | $request = $request->withUploadedFiles( [1 => $file] ); 46 | 47 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 48 | 49 | $this->assertStringContainsString( '"label":"upload.gif"', (string) $response->getBody() ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /i18n/controller/cs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Martin Vyšohlíd , 2016 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: aimeos-core\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 12 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 13 | "Last-Translator: Martin Vyšohlíd , 2016\n" 14 | "Language-Team: Czech (http://app.transifex.com/aimeos/aimeos-core/language/cs/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: cs\n" 19 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" 20 | 21 | #, php-format 22 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 23 | msgstr "Třída \"%1$s\" neimplementuje rozhraní \"%2$s\"" 24 | 25 | #, php-format 26 | msgid "Class \"%1$s\" not available" 27 | msgstr "Třída \"%1$s\" není k dispozici" 28 | 29 | #, php-format 30 | msgid "Class \"%1$s\" not found" 31 | msgstr "Třída \"%1$s\" nenalezena" 32 | 33 | #, php-format 34 | msgid "Downloading file \"%1$s\" failed" 35 | msgstr "" 36 | 37 | #, php-format 38 | msgid "File \"%1$s\" not found" 39 | msgstr "Soubor %1$s nenalezen" 40 | 41 | #, php-format 42 | msgid "File type \"%1$s\" is not allowed" 43 | msgstr "" 44 | 45 | #, php-format 46 | msgid "Invalid characters in class name \"%1$s\"" 47 | msgstr "Zakázané znaky ve jméně třídy \"%1$s\"" 48 | 49 | #, php-format 50 | msgid "No allowed image types configured for \"%1$s\"" 51 | msgstr "Pro \"%1$s\" je nastaven nedovolený typ obrázku" 52 | 53 | #, php-format 54 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 55 | msgstr "Nenalezen žádný typ položky pro \"%1$s/%2$s\" v \"%3$s\"" 56 | 57 | #, php-format 58 | msgid "Unable to create directory \"%1$s\"" 59 | msgstr "" 60 | 61 | #, php-format 62 | msgid "Unable to create file in \"%1$s\"" 63 | msgstr "" 64 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Coupon/Standard.php: -------------------------------------------------------------------------------- 1 | Type::listOf( $this->types()->configOutputType( $domain ) ), 36 | 'args' => [ 37 | ['name' => 'provider', 'type' => Type::string(), 'description' => 'Provider name with decorators separated by comma'], 38 | ], 39 | 'resolve' => $this->getConfig( $domain ), 40 | ]; 41 | 42 | return $list; 43 | } 44 | 45 | 46 | /** 47 | * Returns a closure for returning the provider configuration 48 | * 49 | * @param string $domain Domain path of the manager 50 | * @return \Closure Anonymous method returning one item 51 | */ 52 | protected function getConfig( string $domain ) : \Closure 53 | { 54 | return function( $root, $args, $context ) use ( $domain ) { 55 | 56 | $context = $this->context(); 57 | $groups = $context->config()->get( 'admin/graphql/resource/' . $domain . '/get', [] ); 58 | 59 | if( $context->view()->access( $groups ) !== true ) { 60 | throw new \Aimeos\Admin\Graphql\Exception( 'Forbidden', 403 ); 61 | } 62 | 63 | $manager = \Aimeos\MShop::create( $context, $domain ); 64 | $item = $manager->create()->setProvider( $args['provider'] ); 65 | 66 | return $manager->getProvider( $item, '' )->getConfigBE(); 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Admin/Index/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testAggregateIndex() 27 | { 28 | $body = '{"query":"query {\n aggregateIndex(key: [\"index.catalog.id\"]) {\n aggregates\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | $result = json_decode( (string) $response->getBody(), true ); 33 | $counts = json_decode( $result['data']['aggregateIndex']['aggregates'], true ); 34 | 35 | $this->assertEquals( 4, count( $counts ) ); 36 | $this->assertContains( 4, $counts ); 37 | $this->assertContains( 3, $counts ); 38 | $this->assertContains( 2, $counts ); 39 | } 40 | 41 | 42 | public function testSearchIndex() 43 | { 44 | $id = \Aimeos\MShop::create( $this->context, 'catalog' )->find( 'internet' )->getId; 45 | 46 | $search = addslashes( addslashes( json_encode( ['==' => ['index.catalog.id' => $id]] ) ) ); 47 | $body = '{"query":"query {\n searchIndex(filter: \"' . $search . '\", sort: [\"sort:index.catalog:position()\"]) {\n items {\n id\n code\n }\n total\n }\n}\n","variables":{},"operationName":null}'; 48 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 49 | 50 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 51 | 52 | $this->assertStringContainsString( '"code":"EFGH"', (string) $response->getBody() ); 53 | $this->assertStringContainsString( '"total":20', (string) $response->getBody() ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /i18n/controller/bg.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Plamen Petkov , 2016,2019 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: aimeos-core\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 12 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 13 | "Last-Translator: Plamen Petkov , 2016,2019\n" 14 | "Language-Team: Bulgarian (http://app.transifex.com/aimeos/aimeos-core/language/bg/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: bg\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #, php-format 22 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 23 | msgstr "Класът \"%1$s\" не изпълнява интерфейса \"%2$s\"" 24 | 25 | #, php-format 26 | msgid "Class \"%1$s\" not available" 27 | msgstr "Класът \"%1$s\" не е намерен" 28 | 29 | #, php-format 30 | msgid "Class \"%1$s\" not found" 31 | msgstr "Класът \"%1$s\" не е намерен" 32 | 33 | #, php-format 34 | msgid "Downloading file \"%1$s\" failed" 35 | msgstr "Изтеглянето на файл \"%1$s\" неуспешно" 36 | 37 | #, php-format 38 | msgid "File \"%1$s\" not found" 39 | msgstr "Файл \"%1$s\" не е намерен" 40 | 41 | #, php-format 42 | msgid "File type \"%1$s\" is not allowed" 43 | msgstr "" 44 | 45 | #, php-format 46 | msgid "Invalid characters in class name \"%1$s\"" 47 | msgstr "Недопустими символи в името на класа \"%1$s\"" 48 | 49 | #, php-format 50 | msgid "No allowed image types configured for \"%1$s\"" 51 | msgstr "Неразрешени типове изображения са конфигурирани за \\\"%1$s\\\"" 52 | 53 | #, php-format 54 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 55 | msgstr "Не е намерен тип на елемент за \"%1$s/%2$s\" в \"%3$s\"" 56 | 57 | #, php-format 58 | msgid "Unable to create directory \"%1$s\"" 59 | msgstr "Не е възможно да се създаде директория \"%1$s\"" 60 | 61 | #, php-format 62 | msgid "Unable to create file in \"%1$s\"" 63 | msgstr "Не е възможно да се създаде файл в \"%1$s\"" 64 | -------------------------------------------------------------------------------- /src/GraphQL/Type/Definition/Json.php: -------------------------------------------------------------------------------- 1 | value, true, 512, JSON_THROW_ON_ERROR ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Admin/Graphql/ProviderTrait.php: -------------------------------------------------------------------------------- 1 | Type::listOf( $this->types()->configOutputType( $domain ) ), 41 | 'args' => [ 42 | ['name' => 'provider', 'type' => Type::string(), 'description' => 'Provider name with decorators separated by comma'], 43 | ['name' => 'type', 'type' => Type::string(), 'description' => 'Provider type'], 44 | ], 45 | 'resolve' => $this->getConfig( $domain ), 46 | ]; 47 | 48 | return $list; 49 | } 50 | 51 | 52 | /** 53 | * Returns a closure for returning the provider configuration 54 | * 55 | * @param string $domain Domain path of the manager 56 | * @return \Closure Anonymous method returning one item 57 | */ 58 | protected function getConfig( string $domain ) : \Closure 59 | { 60 | return function( $root, $args, $context ) use ( $domain ) { 61 | 62 | $this->access( $domain, 'get' ); 63 | $manager = \Aimeos\MShop::create( $this->context(), $domain ); 64 | $item = $manager->create()->setProvider( $args['provider'] ); 65 | 66 | return $manager->getProvider( $item, $args['type'] )->getConfigBE(); 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /i18n/controller/es.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Lara Merlotto , 2020 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: aimeos-core\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 12 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 13 | "Last-Translator: Lara Merlotto , 2020\n" 14 | "Language-Team: Spanish (http://app.transifex.com/aimeos/aimeos-core/language/es/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: es\n" 19 | "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 20 | 21 | #, php-format 22 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 23 | msgstr "La clase \"%1$s\" no implementa la interfaz \"%2$s\"" 24 | 25 | #, php-format 26 | msgid "Class \"%1$s\" not available" 27 | msgstr "Clase \"%1$s\" no disponible" 28 | 29 | #, php-format 30 | msgid "Class \"%1$s\" not found" 31 | msgstr "Clase \"%1$s\" no encontrada" 32 | 33 | #, php-format 34 | msgid "Downloading file \"%1$s\" failed" 35 | msgstr "Error en el descargo del archivo %1$s" 36 | 37 | #, php-format 38 | msgid "File \"%1$s\" not found" 39 | msgstr "Archivo \"%1$s\" no encontrado" 40 | 41 | #, php-format 42 | msgid "File type \"%1$s\" is not allowed" 43 | msgstr "" 44 | 45 | #, php-format 46 | msgid "Invalid characters in class name \"%1$s\"" 47 | msgstr "Caracteres no válidos en el nombre de clase \"%1$s\"" 48 | 49 | #, php-format 50 | msgid "No allowed image types configured for \"%1$s\"" 51 | msgstr "No se ha configurado ningún tipo de imagen permitido para \"%1$s\"" 52 | 53 | #, php-format 54 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 55 | msgstr "No se ha encontrado el tipo de elemento para \"%1$s/%2$s\" en \"%3$s\"" 56 | 57 | #, php-format 58 | msgid "Unable to create directory \"%1$s\"" 59 | msgstr "No se ha podido crear el directorio \"%1$s\"" 60 | 61 | #, php-format 62 | msgid "Unable to create file in \"%1$s\"" 63 | msgstr "No se ha podido crear el archivo en \"%1$s\"" 64 | -------------------------------------------------------------------------------- /i18n/controller/de.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Aimeos, 2015-2017,2022 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: aimeos-core\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 12 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 13 | "Last-Translator: Aimeos, 2015-2017,2022\n" 14 | "Language-Team: German (http://app.transifex.com/aimeos/aimeos-core/language/de/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: de\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #, php-format 22 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 23 | msgstr "Klasse \"%1$s\" implementiert nicht Interface \"%2$s\"" 24 | 25 | #, php-format 26 | msgid "Class \"%1$s\" not available" 27 | msgstr "Die Klasse \"%1$s\" wurde nicht gefunden" 28 | 29 | #, php-format 30 | msgid "Class \"%1$s\" not found" 31 | msgstr "Die Klasse \"%1$s\" ist unbekannt" 32 | 33 | #, php-format 34 | msgid "Downloading file \"%1$s\" failed" 35 | msgstr "Herunterladen der Datei \"%1$s\" ist fehl geschlagen" 36 | 37 | #, php-format 38 | msgid "File \"%1$s\" not found" 39 | msgstr "Die Datei \"%1$s\" ist unbekannt" 40 | 41 | #, php-format 42 | msgid "File type \"%1$s\" is not allowed" 43 | msgstr "Der Dateityp \"%1$s\" ist nicht erlaubt" 44 | 45 | #, php-format 46 | msgid "Invalid characters in class name \"%1$s\"" 47 | msgstr "Klassenname \"%1$s\" enthält unerlaubte Zeichen" 48 | 49 | #, php-format 50 | msgid "No allowed image types configured for \"%1$s\"" 51 | msgstr "Es sind keine zulässigen Bildtypen für \"%1$s\" konfiguriert" 52 | 53 | #, php-format 54 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 55 | msgstr "Für den Eintrag \"%1$s/%2$s\" in \"%3$s\" wurde kein Typ gefunden" 56 | 57 | #, php-format 58 | msgid "Unable to create directory \"%1$s\"" 59 | msgstr "Das Verzeichnis \"%1$s\" konnte nicht erstellt werden" 60 | 61 | #, php-format 62 | msgid "Unable to create file in \"%1$s\"" 63 | msgstr "Die Datei \"%1$s\" konnte nicht erstellt werden" 64 | -------------------------------------------------------------------------------- /i18n/controller/hr.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Miro Sertić , 2018 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: aimeos-core\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 12 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 13 | "Last-Translator: Miro Sertić , 2018\n" 14 | "Language-Team: Croatian (http://app.transifex.com/aimeos/aimeos-core/language/hr/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: hr\n" 19 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 20 | 21 | #, php-format 22 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 23 | msgstr "Klasa \"%1$s\" ne implementira sučelje \"%2$s\"" 24 | 25 | #, php-format 26 | msgid "Class \"%1$s\" not available" 27 | msgstr "Klasa \"%1$s\" nije dostupna" 28 | 29 | #, php-format 30 | msgid "Class \"%1$s\" not found" 31 | msgstr "Klasa \"%1$s\" nije pronađjena" 32 | 33 | #, php-format 34 | msgid "Downloading file \"%1$s\" failed" 35 | msgstr "Preuzimanje datoteke \"%1$s\" nije uspjelo" 36 | 37 | #, php-format 38 | msgid "File \"%1$s\" not found" 39 | msgstr "Dokument \"%1$s\" nije pronađjen" 40 | 41 | #, php-format 42 | msgid "File type \"%1$s\" is not allowed" 43 | msgstr "" 44 | 45 | #, php-format 46 | msgid "Invalid characters in class name \"%1$s\"" 47 | msgstr "Nedozvoljen simbol u imenu klase \"%1$s\"" 48 | 49 | #, php-format 50 | msgid "No allowed image types configured for \"%1$s\"" 51 | msgstr "Dopuštene vrste slika nisu konfigurirane \"%1$s\"" 52 | 53 | #, php-format 54 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 55 | msgstr "Nijedna stavka vrste \"%1$s/%2$s\" nije pronađena u \"%3$s\"" 56 | 57 | #, php-format 58 | msgid "Unable to create directory \"%1$s\"" 59 | msgstr "Nemože se stvoriti slijedeći direktorij \"%1$s\"" 60 | 61 | #, php-format 62 | msgid "Unable to create file in \"%1$s\"" 63 | msgstr "Nije moguće stvoriti datoteku \"%1$s\"" 64 | -------------------------------------------------------------------------------- /i18n/controller/hu.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # nzed , 2020,2022 7 | # nzed , 2020 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: aimeos-core\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 13 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 14 | "Last-Translator: nzed , 2020,2022\n" 15 | "Language-Team: Hungarian (http://app.transifex.com/aimeos/aimeos-core/language/hu/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: hu\n" 20 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 21 | 22 | #, php-format 23 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 24 | msgstr "A(z) \"%1$s\" osztály nem valósítja meg a(z) \"%2$s\" felület osztályt" 25 | 26 | #, php-format 27 | msgid "Class \"%1$s\" not available" 28 | msgstr "A/Az \"%1$s\" osztály nem elérhető." 29 | 30 | #, php-format 31 | msgid "Class \"%1$s\" not found" 32 | msgstr "A/Az \"%1$s\" osztály nem található." 33 | 34 | #, php-format 35 | msgid "Downloading file \"%1$s\" failed" 36 | msgstr "\"%1$s\" állomány letöltése nem sikerült" 37 | 38 | #, php-format 39 | msgid "File \"%1$s\" not found" 40 | msgstr "\"%1$s\" állomány nem található" 41 | 42 | #, php-format 43 | msgid "File type \"%1$s\" is not allowed" 44 | msgstr "\"%1$s\" állomány nem engedélyezett" 45 | 46 | #, php-format 47 | msgid "Invalid characters in class name \"%1$s\"" 48 | msgstr "Érvénytelen karakter a/az \"%1$s\" osztály nevében" 49 | 50 | #, php-format 51 | msgid "No allowed image types configured for \"%1$s\"" 52 | msgstr "Nincs beállítva képformátum ehhez: \"%1$s\"" 53 | 54 | #, php-format 55 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 56 | msgstr "\"%1$s/%2$s\" számára a \"%3$s\"-ban/ben nincs fajtatétel" 57 | 58 | #, php-format 59 | msgid "Unable to create directory \"%1$s\"" 60 | msgstr "Nem sikerült létrehozni a könyvtárat \"%1$s\"" 61 | 62 | #, php-format 63 | msgid "Unable to create file in \"%1$s\"" 64 | msgstr "Nem sikerült létrehozni a fájlt \"%1$s\"" 65 | -------------------------------------------------------------------------------- /i18n/controller/fr.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # a270031086f2a0d3514bc0cb507b48f6, 2019 7 | # a270031086f2a0d3514bc0cb507b48f6, 2019 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: aimeos-core\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2022-12-07 11:38+0100\n" 13 | "PO-Revision-Date: 2015-09-29 12:30+0000\n" 14 | "Last-Translator: a270031086f2a0d3514bc0cb507b48f6, 2019\n" 15 | "Language-Team: French (http://app.transifex.com/aimeos/aimeos-core/language/fr/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: fr\n" 20 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 21 | 22 | #, php-format 23 | msgid "Class \"%1$s\" does not implement interface \"%2$s\"" 24 | msgstr "La classe \"%1$s\" n'implémente pas l'interface \"%2$s\"" 25 | 26 | #, php-format 27 | msgid "Class \"%1$s\" not available" 28 | msgstr "Classe \"%1$s\" indisponible" 29 | 30 | #, php-format 31 | msgid "Class \"%1$s\" not found" 32 | msgstr "Classe \"%1$s\" non trouvée" 33 | 34 | #, php-format 35 | msgid "Downloading file \"%1$s\" failed" 36 | msgstr "Le téléchargement du fichier \"%1$s\" a échoué" 37 | 38 | #, php-format 39 | msgid "File \"%1$s\" not found" 40 | msgstr "Fichier \"%1$s\" non trouvé" 41 | 42 | #, php-format 43 | msgid "File type \"%1$s\" is not allowed" 44 | msgstr "" 45 | 46 | #, php-format 47 | msgid "Invalid characters in class name \"%1$s\"" 48 | msgstr "Caractères invalides dans le nom de la catégorie \"%1$s\"" 49 | 50 | #, php-format 51 | msgid "No allowed image types configured for \"%1$s\"" 52 | msgstr "Aucun type d'image autorisé configuré pour \"%1$s\"" 53 | 54 | #, php-format 55 | msgid "No type item for \"%1$s/%2$s\" in \"%3$s\" found" 56 | msgstr "Aucun type d'item trouvé pour \"%1$s/%2$s\" dans \"%3$s\"" 57 | 58 | #, php-format 59 | msgid "Unable to create directory \"%1$s\"" 60 | msgstr "Impossible de créer le répertoire \"%1$s\"" 61 | 62 | #, php-format 63 | msgid "Unable to create file in \"%1$s\"" 64 | msgstr "Impossible de créer le fichier dans \"%1$s\"" 65 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Product/Standard.php: -------------------------------------------------------------------------------- 1 | $this->types()->outputType( $domain ), 36 | 'args' => [ 37 | ['name' => 'code', 'type' => Type::string(), 'description' => 'Unique code'], 38 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 39 | ], 40 | 'resolve' => $this->findItem( $domain ), 41 | ]; 42 | 43 | return $list; 44 | } 45 | 46 | 47 | /** 48 | * Updates the item 49 | * 50 | * @param \Aimeos\MShop\Common\Manager\Iface $manager Manager object for the passed item 51 | * @param \Aimeos\MShop\Common\Item\AdddressRef\Iface $item Item to update 52 | * @param array $entry Associative list of key/value pairs of the item data 53 | * @return \Aimeos\MShop\Common\Item\Iface Updated item 54 | */ 55 | protected function updateItem( \Aimeos\MShop\Common\Manager\Iface $manager, 56 | \Aimeos\MShop\Common\Item\Iface $item, array $entry ) : \Aimeos\MShop\Common\Item\Iface 57 | { 58 | $item = parent::updateItem( $manager, $item, $entry ); 59 | 60 | if( isset( $entry['product.stock'] ) ) 61 | { 62 | $stockItems = $item->getStockItems()->col( null, 'stock.type' ); 63 | 64 | foreach( $entry['product.stock'] as $subentry ) 65 | { 66 | $stockItem = $stockItems->get( $subentry['stock.type'] ?? null ) ?: $manager->createStockItem(); 67 | $item->addStockItem( $stockItem->fromArray( $subentry ) ); 68 | } 69 | 70 | $item->deleteStockItems( $stockItems ); 71 | } 72 | 73 | return $item; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/admin.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'domains' => [ 6 | 'attribute' => 'attribute', 7 | 'attribute/type' => 'attribute/type', 8 | 'attribute/lists/type' => 'attribute/lists/type', 9 | 'attribute/property/type' => 'attribute/property/type', 10 | 'catalog' => 'catalog', 11 | 'catalog/lists/type' => 'catalog/lists/type', 12 | 'coupon' => 'coupon', 13 | 'coupon/code' => 'coupon/code', 14 | 'customer' => 'customer', 15 | 'customer/lists/type' => 'customer/lists/type', 16 | 'customer/property/type' => 'customer/property/type', 17 | 'group' => 'group', 18 | 'index' => 'index', 19 | 'locale' => 'locale', 20 | 'locale/currency' => 'locale/currency', 21 | 'locale/language' => 'locale/language', 22 | 'locale/site' => 'locale/site', 23 | 'media' => 'media', 24 | 'media/type' => 'media/type', 25 | 'media/lists/type' => 'media/lists/type', 26 | 'media/property/type' => 'media/property/type', 27 | 'order' => 'order', 28 | 'order/status' => 'order/status', 29 | 'plugin' => 'plugin', 30 | 'plugin/type' => 'plugin/type', 31 | 'price' => 'price', 32 | 'price/type' => 'price/type', 33 | 'price/lists/type' => 'price/lists/type', 34 | 'price/property/type' => 'price/property/type', 35 | 'product' => 'product', 36 | 'product/type' => 'product/type', 37 | 'product/lists/type' => 'product/lists/type', 38 | 'product/property/type' => 'product/property/type', 39 | 'review' => 'review', 40 | 'rule' => 'rule', 41 | 'rule/type' => 'rule/type', 42 | 'service' => 'service', 43 | 'service/type' => 'service/type', 44 | 'service/lists/type' => 'service/lists/type', 45 | 'stock' => 'stock', 46 | 'stock/type' => 'stock/type', 47 | 'subscription' => 'subscription', 48 | 'supplier' => 'supplier', 49 | 'supplier/lists/type' => 'supplier/lists/type', 50 | 'tag' => 'tag', 51 | 'tag/type' => 'tag/type', 52 | 'text' => 'text', 53 | 'text/type' => 'text/type', 54 | 'text/lists/type' => 'text/lists/type', 55 | 'type' => 'type', 56 | ], 57 | 'lists-domains' => [ 58 | 'attribute' => 'attribute', 59 | 'catalog' => 'catalog', 60 | 'customer' => 'customer', 61 | 'group' => 'group', 62 | 'media' => 'media', 63 | 'price' => 'price', 64 | 'product' => 'product', 65 | 'service' => 'service', 66 | 'supplier' => 'supplier', 67 | 'tag' => 'tag', 68 | 'text' => 'text', 69 | ] 70 | ], 71 | ]; 72 | -------------------------------------------------------------------------------- /tests/Admin/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testFindAttribute() 27 | { 28 | $body = '{"query":"query {\n findAttribute(code: \"xs\",domain: \"product\",type: \"size\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"code":"xs"', (string) $response->getBody() ); 34 | } 35 | 36 | 37 | public function testFindCustomer() 38 | { 39 | $body = '{"query":"query {\n findCustomer(code: \"test@example.com\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 40 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 41 | 42 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 43 | 44 | $this->assertStringContainsString( '"code":"test@example.com"', (string) $response->getBody() ); 45 | } 46 | 47 | 48 | public function testFindService() 49 | { 50 | $body = '{"query":"query {\n findService(code: \"unitdeliverycode\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 51 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 52 | 53 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 54 | 55 | $this->assertStringContainsString( '"code":"unitdeliverycode"', (string) $response->getBody() ); 56 | } 57 | 58 | 59 | public function testFindSupplier() 60 | { 61 | $body = '{"query":"query {\n findSupplier(code: \"unitSupplier001\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 62 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 63 | 64 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 65 | 66 | $this->assertStringContainsString( '"code":"unitSupplier001"', (string) $response->getBody() ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/GraphQL/Type/Definition/Upload.php: -------------------------------------------------------------------------------- 1 | $this->types()->outputType( $domain ), 36 | 'args' => [ 37 | ['name' => 'code', 'type' => Type::string(), 'description' => 'Unique code'], 38 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 39 | ], 40 | 'resolve' => $this->findItem( $domain ), 41 | ]; 42 | 43 | $list['getServiceConfig'] = [ 44 | 'type' => Type::listOf( $this->types()->configOutputType( $domain ) ), 45 | 'args' => [ 46 | ['name' => 'provider', 'type' => Type::string(), 'description' => 'Provider name with decorators separated by comma'], 47 | ['name' => 'type', 'type' => Type::string(), 'description' => 'Provider type ("delivery" or "payment")'], 48 | ], 49 | 'resolve' => $this->getConfig( $domain ), 50 | ]; 51 | 52 | return $list; 53 | } 54 | 55 | 56 | /** 57 | * Returns a closure for returning the provider configuration 58 | * 59 | * @param string $domain Domain path of the manager 60 | * @return \Closure Anonymous method returning one item 61 | */ 62 | protected function getConfig( string $domain ) : \Closure 63 | { 64 | return function( $root, $args, $context ) use ( $domain ) { 65 | 66 | $context = $this->context(); 67 | $groups = $context->config()->get( 'admin/graphql/resource/' . $domain . '/get', [] ); 68 | 69 | if( $context->view()->access( $groups ) !== true ) { 70 | throw new \Aimeos\Admin\Graphql\Exception( 'Forbidden', 403 ); 71 | } 72 | 73 | $manager = \Aimeos\MShop::create( $context, $domain ); 74 | $item = $manager->create()->setProvider( $args['provider'] ); 75 | 76 | return $manager->getProvider( $item, $args['type'] )->getConfigBE(); 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Index/Standard.php: -------------------------------------------------------------------------------- 1 | [ 46 | 'type' => $this->types()->aggregateOutputType( $domain ), 47 | 'args' => [ 48 | ['name' => 'key', 'type' => Type::listOf( Type::string() ), 'description' => 'Aggregation key to group results by, e.g. ["product.status", "index.catalog.id"]'], 49 | ['name' => 'value', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Aggregate values from that column, e.g "index.catalog.id" (optional, only if type is passed)'], 50 | ['name' => 'type', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Type of aggregation like "sum" or "avg" (default: null for count)'], 51 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 52 | ['name' => 'sort', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Sort keys'], 53 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 10000, 'description' => 'Slice size'], 54 | ], 55 | 'resolve' => $this->aggregateItems( $domain ), 56 | ], 57 | 'searchIndex' => [ 58 | 'type' => $this->types()->searchOutputType( 'product' ), 59 | 'args' => [ 60 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 61 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 62 | ['name' => 'sort', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Sort keys'], 63 | ['name' => 'offset', 'type' => Type::int(), 'defaultValue' => 0, 'description' => 'Slice offset'], 64 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 100, 'description' => 'Slice size'], 65 | ], 66 | 'resolve' => $this->searchItems( $domain ), 67 | ] 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Standard.php: -------------------------------------------------------------------------------- 1 | [ 34 | 'type' => Type::string(), 35 | 'args' => [ 36 | ['name' => 'id', 'type' => Type::string(), 'description' => 'Item ID'], 37 | ], 38 | 'resolve' => $this->deleteItems( $domain ), 39 | ], 40 | 'delete' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 41 | 'type' => Type::listOf( Type::string() ), 42 | 'args' => [ 43 | ['name' => 'id', 'type' => Type::listOf( Type::string() ), 'description' => 'List of item IDs'], 44 | ], 45 | 'resolve' => $this->deleteItems( $domain ), 46 | ], 47 | 'save' . str_replace( '/', '', ucwords( $domain, '/' ) ) => [ 48 | 'type' => $this->types()->outputType( $domain ), 49 | 'args' => [ 50 | ['name' => 'input', 'type' => $this->types()->inputType( $domain ), 'description' => 'Item object'], 51 | ], 52 | 'resolve' => $this->saveItem( $domain ), 53 | ], 54 | 'save' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 55 | 'type' => Type::listOf( $this->types()->outputType( $domain ) ), 56 | 'args' => [ 57 | ['name' => 'input', 'type' => Type::listOf( $this->types()->inputType( $domain ) ), 'description' => 'Item objects'], 58 | ], 59 | 'resolve' => $this->saveItems( $domain ), 60 | ] 61 | ]; 62 | } 63 | 64 | 65 | /** 66 | * Returns GraphQL schema definition for the available queries 67 | * 68 | * @param string $domain Domain name of the responsible manager 69 | * @return array GraphQL query schema definition 70 | */ 71 | public function query( string $domain ) : array 72 | { 73 | $list = [ 74 | 'get' . str_replace( '/', '', ucwords( $domain, '/' ) ) => [ 75 | 'type' => $this->types()->outputType( $domain ), 76 | 'args' => [ 77 | ['name' => 'id', 'type' => Type::string(), 'description' => 'Unique ID'], 78 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 79 | ], 80 | 'resolve' => $this->getItem( $domain ), 81 | ], 82 | 'search' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 83 | 'type' => $this->types()->searchOutputType( $domain ), 84 | 'args' => [ 85 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 86 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 87 | ['name' => 'sort', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Sort keys'], 88 | ['name' => 'offset', 'type' => Type::int(), 'defaultValue' => 0, 'description' => 'Slice offset'], 89 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 100, 'description' => 'Slice size'], 90 | ], 91 | 'resolve' => $this->searchItems( $domain ), 92 | ] 93 | ]; 94 | 95 | if( !substr_compare( $domain, '/type', -5 ) ) 96 | { 97 | $list['find' . str_replace( '/', '', ucwords( $domain, '/' ) )] = [ 98 | 'type' => $this->types()->outputType( $domain ), 99 | 'args' => [ 100 | ['name' => 'code', 'type' => Type::string(), 'description' => 'Unique code'], 101 | ['name' => 'domain', 'type' => Type::string(), 'description' => 'Domain of the type'], 102 | ], 103 | 'resolve' => $this->findTypeItem( $domain ), 104 | ]; 105 | } 106 | 107 | return $list; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Media/Standard.php: -------------------------------------------------------------------------------- 1 | $this->types()->outputType( $domain ), 41 | 'args' => [ 42 | ['name' => 'input', 'type' => $this->mediaInputType( $domain ), 'description' => 'Item object'], 43 | ], 44 | 'resolve' => $this->saveItem( $domain ), 45 | ]; 46 | $list['saveMedias'] = [ 47 | 'type' => Type::listOf( $this->types()->outputType( $domain ) ), 48 | 'args' => [ 49 | ['name' => 'input', 'type' => Type::listOf( $this->mediaInputType( $domain ) ), 'description' => 'Item objects'], 50 | ], 51 | 'resolve' => $this->saveItems( $domain ), 52 | ]; 53 | 54 | return $list; 55 | } 56 | 57 | 58 | /** 59 | * Defines the GraphQL media input type 60 | * 61 | * @param string $path Path of the domain manager 62 | * @return \GraphQL\Type\Definition\InputObjectType Input type definition 63 | */ 64 | public function mediaInputType( string $path ) : InputObjectType 65 | { 66 | $name = 'mediaInput'; 67 | 68 | if( isset( $this->type ) ) { 69 | return $this->type; 70 | } 71 | 72 | return $this->type = new InputObjectType( [ 73 | 'name' => $name, 74 | 'fields' => function() use ( $path ) { 75 | 76 | $manager = \Aimeos\MShop::create( $this->context(), $path ); 77 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 78 | $item = $manager->create(); 79 | 80 | if( $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) { 81 | $list['lists'] = $this->types()->listsInputType( $path . '/lists' ); 82 | } 83 | 84 | if( $item instanceof \Aimeos\MShop\Common\Item\PropertyRef\Iface ) { 85 | $list['property'] = Type::listOf( $this->types()->inputType( $path . '/property' ) ); 86 | } 87 | 88 | $list['file'] = ['type' => Upload::type(), 'description' => 'File upload']; 89 | $list['filepreview'] = ['type' => Upload::type(), 'description' => 'Preview file upload']; 90 | 91 | return $list; 92 | }, 93 | 'parseValue' => function( array $values ) use ( $path ) { 94 | return $this->types()->prefix( $path, $values ); 95 | } 96 | ] ); 97 | } 98 | 99 | 100 | /** 101 | * Updates the item 102 | * 103 | * @param \Aimeos\MShop\Common\Manager\Iface $manager Manager object for the passed item 104 | * @param \Aimeos\MShop\Common\Item\AdddressRef\Iface $item Item to update 105 | * @param array $entry Associative list of key/value pairs of the item data 106 | * @return \Aimeos\MShop\Common\Item\Iface Updated item 107 | */ 108 | protected function updateItem( \Aimeos\MShop\Common\Manager\Iface $manager, 109 | \Aimeos\MShop\Common\Item\Iface $item, array $entry ) : \Aimeos\MShop\Common\Item\Iface 110 | { 111 | $item = $item->fromArray( $entry, true ); 112 | 113 | if( isset( $entry['media.file'] ) ) { 114 | $item = $manager->upload( $item, $entry['media.file'], $entry['media.filepreview'] ?? null ); 115 | } 116 | 117 | if( isset( $entry['lists'] ) && $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) { 118 | $item = $this->updateLists( $manager, $item, $entry['lists'] ); 119 | } 120 | 121 | if( isset( $entry['property'] ) && $item instanceof \Aimeos\MShop\Common\Item\PropertyRef\Iface ) { 122 | $item = $this->updateProperties( $manager, $item, $entry['property'] ); 123 | } 124 | 125 | return $item; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Customer/Standard.php: -------------------------------------------------------------------------------- 1 | $this->types()->aggregateOutputType( $domain ), 42 | 'args' => [ 43 | ['name' => 'key', 'type' => Type::listOf( Type::string() ), 'description' => 'Aggregation key to group results by, e.g. "customer.status"'], 44 | ['name' => 'value', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Aggregate values from that column, e.g "customer.status" (optional, only if type is passed)'], 45 | ['name' => 'type', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Type of aggregation like "sum" or "avg" (default: null for count)'], 46 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 47 | ['name' => 'sort', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Sort keys'], 48 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 10000, 'description' => 'Slice size'], 49 | ], 50 | 'resolve' => $this->aggregateItems( $domain ), 51 | ]; 52 | 53 | $list['findCustomer'] = [ 54 | 'type' => $this->types()->outputType( $domain ), 55 | 'args' => [ 56 | ['name' => 'code', 'type' => Type::string(), 'description' => 'Unique code'], 57 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 58 | ], 59 | 'resolve' => $this->findItem( $domain ), 60 | ]; 61 | 62 | return $list; 63 | } 64 | 65 | 66 | /** 67 | * Updates the item 68 | * 69 | * @param \Aimeos\MShop\Common\Manager\Iface $manager Manager object for the passed item 70 | * @param \Aimeos\MShop\Common\Item\AdddressRef\Iface $item Item to update 71 | * @param array $entry Associative list of key/value pairs of the item data 72 | * @return \Aimeos\MShop\Common\Item\Iface Updated item 73 | */ 74 | protected function updateItem( \Aimeos\MShop\Common\Manager\Iface $manager, 75 | \Aimeos\MShop\Common\Item\Iface $item, array $entry ) : \Aimeos\MShop\Common\Item\Iface 76 | { 77 | $view = $this->context()->view(); 78 | $siteId = (string) $this->context()->user()?->getSiteId(); 79 | 80 | if( $view->access( ['super'] ) || strlen( $siteId ) > 0 && !strncmp( $item->getSiteId(), $siteId, strlen( $siteId ) ) ) 81 | { 82 | $item = $item->fromArray( $entry ); 83 | 84 | if( $view->access( ['super', 'admin'] ) ) { 85 | $item->setGroups( array_unique( $entry['groups'] ?? [] ) ); 86 | } 87 | 88 | if( $view->access( ['super', 'admin'] ) || $item->getId() === $this->context()->user() ) 89 | { 90 | !isset( $entry['customer.password'] ) ?: $item->setPassword( $entry['customer.password'] ); 91 | !isset( $entry['customer.code'] ) ?: $item->setCode( $entry['customer.code'] ); 92 | } 93 | 94 | if( isset( $entry['address'] ) && $item instanceof \Aimeos\MShop\Common\Item\AddressRef\Iface ) { 95 | $item = $this->updateAddresses( $manager, $item, $entry['address'] ); 96 | } 97 | 98 | if( isset( $entry['lists'] ) && $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) { 99 | $item = $this->updateLists( $manager, $item, $entry['lists'] ); 100 | } 101 | 102 | if( isset( $entry['property'] ) && $item instanceof \Aimeos\MShop\Common\Item\PropertyRef\Iface ) { 103 | $item = $this->updateProperties( $manager, $item, $entry['property'] ); 104 | } 105 | } 106 | 107 | return $item; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/TestHelper.php: -------------------------------------------------------------------------------- 1 | config(); 45 | } 46 | 47 | $view = new \Aimeos\Base\View\Standard(); 48 | 49 | $helper = new \Aimeos\Base\View\Helper\Access\All( $view ); 50 | $view->addHelper( 'access', $helper ); 51 | 52 | $trans = new \Aimeos\Base\Translation\None( 'de_DE' ); 53 | $helper = new \Aimeos\Base\View\Helper\Translate\Standard( $view, $trans ); 54 | $view->addHelper( 'translate', $helper ); 55 | 56 | $helper = new \Aimeos\Base\View\Helper\Url\Standard( $view, 'http://baseurl' ); 57 | $view->addHelper( 'url', $helper ); 58 | 59 | $helper = new \Aimeos\Base\View\Helper\Session\Standard( $view, self::context( $site )->session() ); 60 | $view->addHelper( 'session', $helper ); 61 | 62 | $helper = new \Aimeos\Base\View\Helper\Number\Standard( $view, '.', '' ); 63 | $view->addHelper( 'number', $helper ); 64 | 65 | $helper = new \Aimeos\Base\View\Helper\Date\Standard( $view, 'Y-m-d' ); 66 | $view->addHelper( 'date', $helper ); 67 | 68 | $paths = ['version', 'client', 'common', 'resource/fs/baseurl', 'resource/fs-media/baseurl', 'resource/fs-theme/baseurl']; 69 | $config = new \Aimeos\Base\Config\Decorator\Protect( $config, $paths ); 70 | $helper = new \Aimeos\Base\View\Helper\Config\Standard( $view, $config ); 71 | $view->addHelper( 'config', $helper ); 72 | 73 | $helper = new \Aimeos\Base\View\Helper\Csrf\Standard( $view, '_csrf_token', '_csrf_value' ); 74 | $view->addHelper( 'csrf', $helper ); 75 | 76 | $psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); 77 | $helper = new \Aimeos\Base\View\Helper\Request\Standard( $view, $psr17Factory->createServerRequest( 'POST', 'https://aimeos.org' ) ); 78 | $view->addHelper( 'request', $helper ); 79 | 80 | $helper = new \Aimeos\Base\View\Helper\Response\Standard( $view, $psr17Factory->createResponse() ); 81 | $view->addHelper( 'response', $helper ); 82 | 83 | return $view; 84 | } 85 | 86 | 87 | /** 88 | * @param string $site 89 | */ 90 | private static function createContext( $site ) 91 | { 92 | $ctx = new \Aimeos\MShop\Context(); 93 | $aimeos = self::getAimeos(); 94 | 95 | 96 | $paths = $aimeos->getConfigPaths(); 97 | $paths[] = __DIR__ . DIRECTORY_SEPARATOR . 'config'; 98 | $file = __DIR__ . DIRECTORY_SEPARATOR . 'confdoc.ser'; 99 | $local = array( 'resource' => array( 'fs' => array( 'adapter' => 'Standard', 'basedir' => __DIR__ . '/tmp' ) ) ); 100 | 101 | $conf = new \Aimeos\Base\Config\PHPArray( [], $paths ); 102 | $conf = new \Aimeos\Base\Config\Decorator\Memory( $conf ); 103 | $conf = new \Aimeos\Base\Config\Decorator\Documentor( $conf, $file ); 104 | $ctx->setConfig( $conf->apply( $local ) ); 105 | 106 | 107 | $logger = new \Aimeos\Base\Logger\File( $site . '.log', \Aimeos\Base\Logger\Iface::DEBUG ); 108 | $ctx->setLogger( $logger ); 109 | 110 | 111 | $dbm = new \Aimeos\Base\DB\Manager\Standard( $conf->get( 'resource', [] ), 'PDO' ); 112 | $ctx->setDatabaseManager( $dbm ); 113 | 114 | 115 | $fs = new \Aimeos\Base\Filesystem\Manager\Standard( $conf->get( 'resource', [] ) ); 116 | $ctx->setFilesystemManager( $fs ); 117 | 118 | 119 | $mq = new \Aimeos\Base\MQueue\Manager\Standard( $conf->get( 'resource', [] ) ); 120 | $ctx->setMessageQueueManager( $mq ); 121 | 122 | 123 | $cache = new \Aimeos\Base\Cache\None(); 124 | $ctx->setCache( $cache ); 125 | 126 | 127 | $i18n = new \Aimeos\Base\Translation\None( 'de' ); 128 | $ctx->setI18n( array( 'de' => $i18n ) ); 129 | 130 | 131 | $passwd = new \Aimeos\Base\Password\Standard(); 132 | $ctx->setPassword( $passwd ); 133 | 134 | 135 | $session = new \Aimeos\Base\Session\None(); 136 | $ctx->setSession( $session ); 137 | 138 | 139 | $localeManager = \Aimeos\MShop::create( $ctx, 'locale' ); 140 | $locale = $localeManager->bootstrap( $site, '', '', false ); 141 | $ctx->setLocale( $locale ); 142 | 143 | 144 | return $ctx->setEditor( 'ai-admin-graphql' ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/Admin/Order/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testAggregateOrders() 27 | { 28 | $body = '{"query":"query {\n aggregateOrders(key: [\"order.price\"]) {\n aggregates\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"aggregates":"{\"13.50\":1,\"2400.00\":1,\"672.00\":1,\"53.50\":1}"', (string) $response->getBody() ); 34 | } 35 | 36 | 37 | public function testAggregateOrdersNested() 38 | { 39 | $body = '{"query":"query {\n aggregateOrders(key: [\"order.statuspayment\", \"order.price\"]) {\n aggregates\n }\n}\n","variables":{},"operationName":null}'; 40 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 41 | 42 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 43 | 44 | $this->assertStringContainsString( '"aggregates":"{\"5\":{\"13.50\":1},\"6\":{\"2400.00\":1,\"672.00\":1,\"53.50\":1}}"', (string) $response->getBody() ); 45 | } 46 | 47 | 48 | public function testAggregateOrdersNestedSum() 49 | { 50 | $body = '{"query":"query {\n aggregateOrders(key: [\"order.statuspayment\", \"order.price\"], value: \"order.price\", type: \"sum\") {\n aggregates\n }\n}\n","variables":{},"operationName":null}'; 51 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 52 | 53 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 54 | 55 | $this->assertStringContainsString( '"aggregates":"{\"5\":{\"13.50\":\"13.50\"},\"6\":{\"2400.00\":\"2400.00\",\"672.00\":\"672.00\",\"53.50\":\"53.50\"}}"', (string) $response->getBody() ); 56 | } 57 | 58 | 59 | public function testSearchOrders() 60 | { 61 | $body = '{"query":"query {\n searchOrders(filter: \"{}\", include: [\"order/address\",\"order/coupon\",\"order/product\",\"order/service\",\"order/status\"]) {\n items {\n id\n sitecode\n address {\n id\n }\n coupon {\n code\n }\n product {\n id\n attribute {\n id\n }\n product {\n id\n attribute {\n id\n }\n }\n }\n service {\n id\n attribute {\n id\n }\n transaction {\n id\n }\n }\n status {\n id\n }\n }\n total\n }\n}\n","variables":{},"operationName":null}'; 62 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 63 | 64 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 65 | $result = json_decode( (string) $response->getBody(), true ); 66 | $items = $result['data']['searchOrders']['items'] ?? []; 67 | 68 | $this->assertEquals( $result['data']['searchOrders']['total'], count( $items ) ); 69 | $this->assertEquals( 'unittest', $items[0]['sitecode'] ); 70 | $this->assertEquals( 2, count( $items[0]['address'] ) ); 71 | $this->assertEquals( 2, count( $items[0]['coupon'] ) ); 72 | $this->assertEquals( 4, count( $items[0]['product'] ) ); 73 | $this->assertEquals( 3, count( $items[0]['product'][0]['attribute'] ) ); 74 | $this->assertEquals( 0, count( $items[0]['product'][0]['product'] ) ); 75 | $this->assertEquals( 2, count( $items[0]['service'] ) ); 76 | $this->assertEquals( 0, count( $items[0]['service'][0]['attribute'] ) ); 77 | $this->assertEquals( 1, count( $items[0]['status'] ) ); 78 | } 79 | 80 | 81 | public function testSaveOrder() 82 | { 83 | $stub = $this->getMockBuilder( '\\Aimeos\\MShop\\Order\\Manager\\Standard' ) 84 | ->setConstructorArgs( array( $this->context ) ) 85 | ->onlyMethods( ['save'] ) 86 | ->getMock(); 87 | 88 | $stub->expects( $this->once() )->method( 'save' )->willReturn( $stub->create( ['order.id' => 123, 'order.channel' => 'unittest'] ) ); 89 | 90 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Order\\Manager\\Standard', $stub ); 91 | 92 | $body = '{"query":"mutation {\n saveOrder(input: {\n channel: \"unittest\"\n }) {\n id\n channel\n }\n}\n","variables":{},"operationName":null}'; 93 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 94 | 95 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 96 | 97 | $this->assertStringContainsString( '"channel":"unittest"', (string) $response->getBody() ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Admin/Graphql/UpdateTrait.php: -------------------------------------------------------------------------------- 1 | getAddresses()->reverse(); 34 | 35 | foreach( $entries as $subentry ) 36 | { 37 | $address = $addressItems->pop() ?: $manager->createAddressItem(); 38 | $item->addAddressItem( $address->fromArray( $subentry ) ); 39 | } 40 | 41 | return $item->deleteAddressItems( $addressItems ); 42 | } 43 | 44 | 45 | /** 46 | * Updates the item 47 | * 48 | * @param \Aimeos\MShop\Common\Manager\Iface $manager Manager object for the passed item 49 | * @param \Aimeos\MShop\Common\Item\AdddressRef\Iface $item Item to update 50 | * @param array $entry Associative list of key/value pairs of the item data 51 | * @return \Aimeos\MShop\Common\Item\Iface Updated item 52 | */ 53 | protected function updateItem( \Aimeos\MShop\Common\Manager\Iface $manager, 54 | \Aimeos\MShop\Common\Item\Iface $item, array $entry ) : \Aimeos\MShop\Common\Item\Iface 55 | { 56 | $item = $item->fromArray( $entry, true ); 57 | 58 | if( isset( $entry['address'] ) && $item instanceof \Aimeos\MShop\Common\Item\AddressRef\Iface ) { 59 | $item = $this->updateAddresses( $manager, $item, $entry['address'] ); 60 | } 61 | 62 | if( isset( $entry['lists'] ) && $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) { 63 | $item = $this->updateLists( $manager, $item, $entry['lists'] ); 64 | } 65 | 66 | if( isset( $entry['property'] ) && $item instanceof \Aimeos\MShop\Common\Item\PropertyRef\Iface ) { 67 | $item = $this->updateProperties( $manager, $item, $entry['property'] ); 68 | } 69 | 70 | return $item; 71 | } 72 | 73 | 74 | /** 75 | * Updates the list references of the item 76 | * 77 | * @param \Aimeos\MShop\Common\Manager\Iface $manager Manager object for the passed item 78 | * @param \Aimeos\MShop\Common\Item\ListsRef\Iface $item Item to update 79 | * @param array $entries List of entries with key/value pairs of the reference data 80 | * @return \Aimeos\MShop\Common\Item\Iface Updated item 81 | */ 82 | protected function updateLists( \Aimeos\MShop\Common\Manager\Iface $manager, 83 | \Aimeos\MShop\Common\Item\ListsRef\Iface $item, array $entries ) : \Aimeos\MShop\Common\Item\Iface 84 | { 85 | $resource = $item->getResourceType(); 86 | 87 | foreach( $entries as $domain => $list ) 88 | { 89 | $domainManager = \Aimeos\MShop::create( $this->context(), $domain ); 90 | $listItems = $item->getListItems( $domain, null, null, false ); 91 | $refItems = $item->getRefItems( $domain, null, null, false ); 92 | 93 | foreach( $list as $subentry ) 94 | { 95 | $refItem = null; 96 | $listId = $subentry[$resource . '.lists.id'] ?? ''; 97 | $listType = $subentry[$resource . '.lists.type'] ?? 'default'; 98 | $refId = $subentry['item'][$domain.'.id'] ?? $subentry[$resource . '.lists.refid'] ?? ''; 99 | 100 | $listItem = $listItems->get( $listId ) ?? $item->getListItem( $domain, $listType, $refId ) ?? $manager->createListItem(); 101 | 102 | if ( isset( $subentry['item'] ) ) { 103 | $refItem = ( $listItem->getRefItem() ?? $refItems->get( $refId ) ?? $domainManager->create() )->fromArray( $subentry['item'], true ); 104 | } 105 | 106 | if( isset( $subentry['item']['address'] ) && $refItem instanceof \Aimeos\MShop\Common\Item\AddressRef\Iface ) { 107 | $refItem = $this->updateAddresses( $domainManager, $refItem, $subentry['item']['address'] ); 108 | } 109 | 110 | if( isset( $subentry['item']['lists'] ) && $refItem instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) { 111 | $refItem = $this->updateLists( $domainManager, $refItem, $subentry['item']['lists'] ); 112 | } 113 | 114 | if( isset( $subentry['item']['property'] ) && $refItem instanceof \Aimeos\MShop\Common\Item\PropertyRef\Iface ) { 115 | $refItem = $this->updateProperties( $domainManager, $refItem, $subentry['item']['property'] ); 116 | } 117 | 118 | $item->addListItem( $domain, $listItem->fromArray( $subentry, true ), $refItem ); 119 | unset( $listItems[$listItem->getId()] ); 120 | } 121 | 122 | $item->deleteListItems( $listItems ); 123 | } 124 | 125 | return $item; 126 | } 127 | 128 | 129 | /** 130 | * Updates the properties of the item 131 | * 132 | * @param \Aimeos\MShop\Common\Manager\Iface $manager Manager object for the passed item 133 | * @param \Aimeos\MShop\Common\Item\ListsRef\Iface $item Item to update 134 | * @param array $entries List of entries with key/value pairs of the property data 135 | * @return \Aimeos\MShop\Common\Item\Iface Updated item 136 | */ 137 | protected function updateProperties( \Aimeos\MShop\Common\Manager\Iface $manager, 138 | \Aimeos\MShop\Common\Item\PropertyRef\Iface $item, array $entries ) : \Aimeos\MShop\Common\Item\Iface 139 | { 140 | $propItems = $item->getPropertyItems()->reverse(); 141 | 142 | foreach( $entries as $subentry ) 143 | { 144 | $propItem = $propItems->pop() ?: $manager->createPropertyItem(); 145 | $item->addPropertyItem( $propItem->fromArray( $subentry ) ); 146 | } 147 | 148 | return $item->deletePropertyItems( $propItems ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Admin/Graphql.php: -------------------------------------------------------------------------------- 1 | config()->get( 'admin/graphql/debug', false ); 55 | 56 | if( !is_array( $input = $request->getParsedBody() ) ) 57 | { 58 | if( empty( $input = json_decode( $request->getBody()->getContents() ?? '', true ) ) ) { 59 | throw new \Aimeos\Admin\Graphql\Exception( 'Invalid input' ); 60 | } 61 | } 62 | 63 | if( isset( $input['operations'] ) ) 64 | { 65 | $map = json_decode( $input['map'] ?? '[]', true ); 66 | $operations = json_decode( $input['operations'], true ); 67 | $operations = self::files( $request->getUploadedFiles(), $operations, $map ); 68 | 69 | $opname = $operations['operationName'] ?? null; 70 | $variables = $operations['variables'] ?? []; 71 | $query = $operations['query'] ?? ''; 72 | } 73 | else 74 | { 75 | $opname = $input['operationName'] ?? null; 76 | $variables = $input['variables'] ?? []; 77 | $query = $input['query'] ?? ''; 78 | } 79 | 80 | $result = GraphQLBase::executeQuery( 81 | self::schema( $context ), 82 | $query, 83 | [], // root 84 | null, // context 85 | $variables, 86 | $opname 87 | )->toArray( $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : 0 ); 88 | } 89 | catch( \Throwable $t ) 90 | { 91 | $error = ['message' => $t->getMessage()]; 92 | 93 | if( $debug ) { 94 | $error['locations'] = $t->getTrace(); 95 | } 96 | 97 | $result = ['errors' => [$error]]; 98 | } 99 | 100 | $body = \Nyholm\Psr7\Stream::create( json_encode( $result ) ); 101 | return ( new Psr17Factory )->createResponse()->withBody( $body ); 102 | } 103 | 104 | 105 | /** 106 | * Maps the files to the corresponding variables 107 | * 108 | * @param array $files List of \Psr\Http\Message\UploadedFileInterface objects 109 | * @param array $operations GraphQL operations with "variables" section 110 | * @param array $map GraphQL variable mapping 111 | * @return array Mapped operations array 112 | * @see https://github.com/Ecodev/graphql-upload 113 | */ 114 | protected static function files( array $files, array $operations, array $map ) : array 115 | { 116 | foreach( $map as $fileKey => $locations ) 117 | { 118 | foreach( $locations as $location ) 119 | { 120 | $items = &$operations; 121 | 122 | foreach( explode( '.', $location ) as $key ) 123 | { 124 | if( !isset( $items[$key] ) || !is_array( $items[$key] ) ) { 125 | $items[$key] = []; 126 | } 127 | 128 | $items = &$items[$key]; 129 | } 130 | 131 | if( !array_key_exists( $fileKey, $files ) ) 132 | { 133 | throw new \Aimeos\Admin\Graphql\Exception( 134 | "GraphQL query declared an upload in `$location`, but no corresponding file were actually uploaded" 135 | ); 136 | } 137 | 138 | $items = $files[$fileKey]; 139 | } 140 | } 141 | 142 | return $operations; 143 | } 144 | 145 | 146 | /** 147 | * Returns the GraphQL schema 148 | * 149 | * @param \Aimeos\MShop\ContextIface $context Context object 150 | * @return \GraphQL\Type\Schema List of GraphQL query schema definitions 151 | */ 152 | protected static function schema( \Aimeos\MShop\ContextIface $context ) : \GraphQL\Type\Schema 153 | { 154 | $query = $mutation = []; 155 | $config = $context->config(); 156 | $registry = new \Aimeos\Admin\Graphql\Registry( $context ); 157 | 158 | $stdname = $config->get( 'admin/graphql/name', 'Standard' ); 159 | $domains = $config->get( 'admin/graphql/domains', [] ); 160 | 161 | foreach( $domains as $domain ) 162 | { 163 | $name = $config->get( 'admin/graphql/' . $domain . '/name', 'Standard' ); 164 | $classname = '\Aimeos\Admin\Graphql\\' . str_replace( '/', '\\', ucwords( $domain, '/' ) ) . '\\' . $name; 165 | 166 | if( !class_exists( $classname ) ) { 167 | $classname = '\Aimeos\Admin\Graphql\\' . $stdname; 168 | } 169 | 170 | $object = new $classname( $context, $registry ); 171 | 172 | $mutation = array_replace_recursive( $mutation, $object->mutation( $domain ) ); 173 | $query = array_replace_recursive( $query, $object->query( $domain ) ); 174 | } 175 | 176 | return new Schema([ 177 | 'query' => new ObjectType( [ 178 | 'name' => 'query', 179 | 'fields' => $query 180 | ] ), 181 | 'mutation' => new ObjectType( [ 182 | 'name' => 'mutation', 183 | 'fields' => $mutation 184 | ] ), 185 | ] ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /tests/Admin/Locale/Site/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testFindLocaleSite() 27 | { 28 | $body = '{"query":"query {\n findLocaleSite(code: \"unittest\") {\n id\n code\n hasChildren\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"code":"unittest"', (string) $response->getBody() ); 34 | } 35 | 36 | 37 | public function testGetLocaleSitePath() 38 | { 39 | $id = \Aimeos\MShop::create( $this->context, 'locale/site' )->find( 'unittest' ); 40 | 41 | $body = '{"query":"query {\n getLocaleSitePath(id: \"' . $id . '\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 42 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 43 | 44 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 45 | 46 | $this->assertStringContainsString( '"code":"unittest"', (string) $response->getBody() ); 47 | } 48 | 49 | 50 | public function testGetLocaleSiteTree() 51 | { 52 | $id = \Aimeos\MShop::create( $this->context, 'locale/site' )->find( 'unittest' ); 53 | 54 | $body = '{"query":"query {\n getLocaleSiteTree(id: \"' . $id . '\") {\n id\n code\n children {\n id\n code\n }\n}\n}\n","variables":{},"operationName":null}'; 55 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 56 | 57 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 58 | 59 | $this->assertStringContainsString( '"code":"unittest"', (string) $response->getBody() ); 60 | } 61 | 62 | 63 | public function testGetLocaleSiteTreeLevel() 64 | { 65 | $id = \Aimeos\MShop::create( $this->context, 'locale/site' )->find( 'unittest' ); 66 | 67 | $body = '{"query":"query {\n getLocaleSiteTree(id: \"' . $id . '\", level: 2) {\n id\n code\n children {\n id\n code\n }\n}\n}\n","variables":{},"operationName":null}'; 68 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 69 | 70 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 71 | 72 | $this->assertStringContainsString( '"code":"unittest"', (string) $response->getBody() ); 73 | } 74 | 75 | 76 | public function testInsertLocaleSite() 77 | { 78 | $stub = $this->getMockBuilder( '\\Aimeos\\MShop\\Locale\\Manager\\Site\\Standard' ) 79 | ->setConstructorArgs( array( $this->context ) ) 80 | ->onlyMethods( ['insert'] ) 81 | ->getMock(); 82 | 83 | $item = $stub->create( ['locale.site.code' => 'test-graphql'] ); 84 | $stub->expects( $this->once() )->method( 'insert' )->willReturn( $item ); 85 | 86 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Locale\\Manager\\Site\\Standard', $stub ); 87 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Locale\\Manager\\Site\\Sites', $stub ); 88 | 89 | $body = '{"query":"mutation {\n insertLocaleSite(input: {\n code: \"test-graphql\"\n }) {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 90 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 91 | 92 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 93 | 94 | $this->assertStringContainsString( '"code":"test-graphql"', (string) $response->getBody() ); 95 | } 96 | 97 | 98 | public function testMoveLocaleSite() 99 | { 100 | $stub = $this->getMockBuilder( '\\Aimeos\\MShop\\Locale\\Manager\\Site\\Standard' ) 101 | ->setConstructorArgs( array( $this->context ) ) 102 | ->onlyMethods( ['move'] ) 103 | ->getMock(); 104 | 105 | $stub->expects( $this->once() )->method( 'move' ); 106 | 107 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Locale\\Manager\\Site\\Standard', $stub ); 108 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Locale\\Manager\\Site\\Sites', $stub ); 109 | 110 | $body = '{"query":"mutation {\n moveLocaleSite(id: \"1\", parentid: null)\n}\n","variables":{},"operationName":null}'; 111 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 112 | 113 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 114 | 115 | $this->assertStringContainsString( '"moveLocaleSite":"1"', (string) $response->getBody() ); 116 | } 117 | 118 | 119 | public function testSearchLocaleSite() 120 | { 121 | $search = addslashes( addslashes( json_encode( ['==' => ['locale.site.code' => 'unittest']] ) ) ); 122 | $body = '{"query":"query {\n searchLocaleSites(filter: \"' . $search . '\") {\n items {\n id\n code\n }\n total\n }\n}\n","variables":{},"operationName":null}'; 123 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 124 | 125 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 126 | 127 | $this->assertStringContainsString( '"code":"unittest"', (string) $response->getBody() ); 128 | } 129 | 130 | 131 | public function testSearchLocaleSiteTree() 132 | { 133 | $search = addslashes( addslashes( json_encode( ['==' => ['locale.site.code' => 'unittest']] ) ) ); 134 | $body = '{"query":"query {\n searchLocaleSiteTree(filter: \"' . $search . '\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 135 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 136 | 137 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 138 | $result = json_decode( (string) $response->getBody(), true ); 139 | 140 | $this->assertEquals( 1, count( $result['data']['searchLocaleSiteTree'] ) ); 141 | $this->assertEquals( 'unittest', $result['data']['searchLocaleSiteTree'][0]['code'] ); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /tests/Admin/Product/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testGetProduct() 27 | { 28 | $id = \Aimeos\MShop::create( $this->context, 'product' )->find( 'CNC' ); 29 | 30 | $body = '{"query":"query {\n getProduct(id: \"' . $id . '\", include: [\"stock\"]) {\n id\n code\n stock {\n type\n }\n }\n}\n","variables":{},"operationName":null}'; 31 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 32 | 33 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 34 | 35 | $this->assertStringContainsString( '"code":"CNC"', (string) $response->getBody() ); 36 | $this->assertStringContainsString( '"stock":[{"type":"default"}]', (string) $response->getBody() ); 37 | } 38 | 39 | 40 | public function testFindProduct() 41 | { 42 | $body = '{"query":"query {\n findProduct(code: \"CNC\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 43 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 44 | 45 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 46 | 47 | $this->assertStringContainsString( '"code":"CNC"', (string) $response->getBody() ); 48 | } 49 | 50 | 51 | public function testFindProductType() 52 | { 53 | $body = '{"query":"query {\n findProductType(code: \"default\", domain: \"product\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 54 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 55 | 56 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 57 | 58 | $this->assertStringContainsString( '"code":"default"', (string) $response->getBody() ); 59 | } 60 | 61 | 62 | public function testSearchProducts() 63 | { 64 | $manager = \Aimeos\MShop::create( $this->context, 'product' ); 65 | $filter = $manager->filter()->add( 'product.code', '==', ['CNC', 'CNE'] ); 66 | $ids = $manager->search( $filter )->keys()->all(); 67 | 68 | $search = addslashes( addslashes( json_encode( ['==' => ['product.id' => $ids]] ) ) ); 69 | $body = '{"query":"query {\n searchProducts(filter: \"' . $search . '\", include: [\"stock\"]) {\n items {\n id\n code\n stock {\n type\n }\n }\n total\n }\n}\n","variables":{},"operationName":null}'; 70 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 71 | 72 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 73 | 74 | $this->assertStringContainsString( '"code":"CNC"', (string) $response->getBody() ); 75 | $this->assertStringContainsString( '"code":"CNE"', (string) $response->getBody() ); 76 | $this->assertStringContainsString( '"stock":[{"type":"default"}]', (string) $response->getBody() ); 77 | } 78 | 79 | 80 | public function testSaveProduct() 81 | { 82 | $stockStub = $this->getMockBuilder( '\\Aimeos\\MShop\\Stock\\Manager\\Standard' ) 83 | ->setConstructorArgs( array( $this->context ) ) 84 | ->onlyMethods( ['save'] ) 85 | ->getMock(); 86 | 87 | $stockStub->expects( $this->once() )->method( 'save' )->willReturnCallback( fn( $item ) => $item->setId( 123 ) ); 88 | 89 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Stock\\Manager\\Standard', $stockStub ); 90 | 91 | $body = '{"query":"mutation {\n saveProduct(input: {\n code: \"test-graphql\"\n, stock: {\n type: \"default\" stocklevel: 100\n}\n }) {\n id\n code\n stock {\n id\n type\n stocklevel\n }\n }\n}\n","variables":{},"operationName":null}'; 92 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 93 | 94 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 95 | $result = json_decode( (string) $response->getBody(), true ); 96 | 97 | $this->assertTrue( isset( $result['data']['saveProduct']['id'] ) ); 98 | \Aimeos\MShop::create( $this->context, 'product' )->delete( $result['data']['saveProduct']['id'] ); 99 | 100 | $this->assertStringContainsString( '"code":"test-graphql"', (string) $response->getBody() ); 101 | $this->assertStringContainsString( '"id":"123"', (string) $response->getBody() ); 102 | $this->assertStringContainsString( '"type":"default"', (string) $response->getBody() ); 103 | $this->assertStringContainsString( '"stocklevel":100', (string) $response->getBody() ); 104 | } 105 | 106 | 107 | public function testSaveProducts() 108 | { 109 | $stub = $this->getMockBuilder( '\\Aimeos\\MShop\\Product\\Manager\\Standard' ) 110 | ->setConstructorArgs( array( $this->context ) ) 111 | ->onlyMethods( ['save', 'type'] ) 112 | ->getMock(); 113 | 114 | $stub->expects( $this->once() )->method( 'save' )->willReturnArgument( 0 ); 115 | $stub->method( 'type' )->willReturn( ['product'] ); 116 | 117 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Product\\Manager\\Standard', $stub ); 118 | 119 | $body = '{"query":"mutation {\n saveProducts(input: [{\n code: \"test-graphql\"\n }]) {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 120 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 121 | 122 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 123 | 124 | $this->assertStringContainsString( '"code":"test-graphql"', (string) $response->getBody() ); 125 | } 126 | 127 | 128 | public function testSaveProductLists() 129 | { 130 | $stub = $this->getMockBuilder( '\\Aimeos\\MShop\\Product\\Manager\\Standard' ) 131 | ->setConstructorArgs( array( $this->context ) ) 132 | ->onlyMethods( ['save', 'type'] ) 133 | ->getMock(); 134 | 135 | $stub->expects( $this->once() )->method( 'save' )->willReturnArgument( 0 ); 136 | $stub->method( 'type' )->willReturn( ['product'] ); 137 | $stub = new \Aimeos\MShop\Common\Manager\Decorator\Lists( $stub, $this->context ); 138 | 139 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Product\\Manager\\Standard', $stub ); 140 | 141 | $body = '{"query":"mutation {\n saveProduct(input: {\n code: \"test-graphql\"\n lists: {\n group: {\n id: \"123\"\n item: {\n id: \"1\"\n code: \"test-group\"\n }\n }\n }\n }) {\n id\n code\n lists {\n group {\n id\n item {\n id\n code\n }\n }\n }\n }\n}\n","variables":{},"operationName":null}'; 142 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 143 | 144 | $body = (string) \Aimeos\Admin\Graphql::execute( $this->context, $request )->getBody(); 145 | 146 | $this->assertStringContainsString( '"code":"test-graphql"', $body ); 147 | $this->assertStringContainsString( '"code":"test-group"', $body ); 148 | $this->assertStringContainsString( '"id":"123"', $body ); 149 | $this->assertStringContainsString( '"id":"1"', $body ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # PHP CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-php/ for more details 4 | # 5 | version: 2.1 6 | 7 | jobs: 8 | "php81-mysql": 9 | docker: 10 | - image: aimeos/ci-php:8.1 11 | - image: mysql:latest 12 | environment: 13 | MYSQL_ROOT_PASSWORD: rootpw 14 | MYSQL_DATABASE: aimeos 15 | MYSQL_USER: aimeos 16 | MYSQL_PASSWORD: aimeos 17 | steps: 18 | - checkout 19 | - run: git clone --depth=50 --branch=master https://github.com/aimeos/aimeos-core ../aimeos-core 20 | - run: cd .. && mv project aimeos-core/ext/ai-admin-graphql && mv aimeos-core project && cd project 21 | - restore_cache: 22 | keys: 23 | - php81-{{ checksum "composer.json" }} 24 | - run: composer req --no-update webonyx/graphql-php 25 | - run: composer update -n --prefer-dist 26 | - save_cache: 27 | key: php81-{{ checksum "composer.json" }} 28 | paths: [./vendor] 29 | - run: echo " array( 'adapter' => 'mysql', 'host' => '127.0.0.1', 'port' => 3306, 'database' => 'aimeos', 'username' => 'aimeos', 'password' => 'aimeos', 'limit' => 2, 'opt-persistent' => false, 'stmt' => array( \"SET SESSIOn sort_buffer_size=2097144; SET NAMES 'utf8'; SET SESSION sql_mode='ANSI'\" ) ), 'fs' => array( 'adapter' => 'Standard', 'basedir' => '.' ), 'mq' => array( 'adapter' => 'Standard', 'db' => 'db' ) );" > config/resource.php 30 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 31 | - run: ./vendor/bin/phing -Ddir=ext/ai-admin-graphql setup testext 32 | 33 | "php82-mysql": 34 | docker: 35 | - image: aimeos/ci-php:8.2 36 | - image: mysql:latest 37 | environment: 38 | MYSQL_ROOT_PASSWORD: rootpw 39 | MYSQL_DATABASE: aimeos 40 | MYSQL_USER: aimeos 41 | MYSQL_PASSWORD: aimeos 42 | steps: 43 | - checkout 44 | - run: git clone --depth=50 --branch=master https://github.com/aimeos/aimeos-core ../aimeos-core 45 | - run: cd .. && mv project aimeos-core/ext/ai-admin-graphql && mv aimeos-core project && cd project 46 | - restore_cache: 47 | keys: 48 | - php82-{{ checksum "composer.json" }} 49 | - run: composer req --no-update webonyx/graphql-php 50 | - run: composer update -n --prefer-dist 51 | - save_cache: 52 | key: php82-{{ checksum "composer.json" }} 53 | paths: [./vendor] 54 | - run: echo " array( 'adapter' => 'mysql', 'host' => '127.0.0.1', 'port' => 3306, 'database' => 'aimeos', 'username' => 'aimeos', 'password' => 'aimeos', 'limit' => 2, 'opt-persistent' => false, 'stmt' => array( \"SET SESSIOn sort_buffer_size=2097144; SET NAMES 'utf8'; SET SESSION sql_mode='ANSI'\" ) ), 'fs' => array( 'adapter' => 'Standard', 'basedir' => '.' ), 'mq' => array( 'adapter' => 'Standard', 'db' => 'db' ) );" > config/resource.php 55 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 56 | - run: ./vendor/bin/phing -Ddir=ext/ai-admin-graphql setup coverageext 57 | - run: ./vendor/bin/php-coveralls --root_dir ext/ai-admin-graphql -o tests/coveralls.json -vvv -x tests/coverage.xml 58 | 59 | "php83-mysql": 60 | docker: 61 | - image: aimeos/ci-php:8.3 62 | - image: mysql:latest 63 | environment: 64 | MYSQL_ROOT_PASSWORD: rootpw 65 | MYSQL_DATABASE: aimeos 66 | MYSQL_USER: aimeos 67 | MYSQL_PASSWORD: aimeos 68 | steps: 69 | - checkout 70 | - run: git clone --depth=50 --branch=master https://github.com/aimeos/aimeos-core ../aimeos-core 71 | - run: cd .. && mv project aimeos-core/ext/ai-admin-graphql && mv aimeos-core project && cd project 72 | - restore_cache: 73 | keys: 74 | - php83-{{ checksum "composer.json" }} 75 | - run: composer req --no-update webonyx/graphql-php 76 | - run: composer update -n --prefer-dist 77 | - save_cache: 78 | key: php83-{{ checksum "composer.json" }} 79 | paths: [./vendor] 80 | - run: echo " array( 'adapter' => 'mysql', 'host' => '127.0.0.1', 'port' => 3306, 'database' => 'aimeos', 'username' => 'aimeos', 'password' => 'aimeos', 'limit' => 2, 'opt-persistent' => false, 'stmt' => array( \"SET SESSIOn sort_buffer_size=2097144; SET NAMES 'utf8'; SET SESSION sql_mode='ANSI'\" ) ), 'fs' => array( 'adapter' => 'Standard', 'basedir' => '.' ), 'mq' => array( 'adapter' => 'Standard', 'db' => 'db' ) );" > config/resource.php 81 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 82 | - run: ./vendor/bin/phing -Ddir=ext/ai-admin-graphql setup testext 83 | 84 | "php84-mysql": 85 | docker: 86 | - image: aimeos/ci-php:8.4 87 | - image: mysql:latest 88 | environment: 89 | MYSQL_ROOT_PASSWORD: rootpw 90 | MYSQL_DATABASE: aimeos 91 | MYSQL_USER: aimeos 92 | MYSQL_PASSWORD: aimeos 93 | steps: 94 | - checkout 95 | - run: git clone --depth=50 --branch=master https://github.com/aimeos/aimeos-core ../aimeos-core 96 | - run: cd .. && mv project aimeos-core/ext/ai-admin-graphql && mv aimeos-core project && cd project 97 | - restore_cache: 98 | keys: 99 | - php84-{{ checksum "composer.json" }} 100 | - run: composer req --no-update webonyx/graphql-php 101 | - run: composer update -n --prefer-dist 102 | - save_cache: 103 | key: php84-{{ checksum "composer.json" }} 104 | paths: [./vendor] 105 | - run: echo " array( 'adapter' => 'mysql', 'host' => '127.0.0.1', 'port' => 3306, 'database' => 'aimeos', 'username' => 'aimeos', 'password' => 'aimeos', 'limit' => 2, 'opt-persistent' => false, 'stmt' => array( \"SET SESSIOn sort_buffer_size=2097144; SET NAMES 'utf8'; SET SESSION sql_mode='ANSI'\" ) ), 'fs' => array( 'adapter' => 'Standard', 'basedir' => '.' ), 'mq' => array( 'adapter' => 'Standard', 'db' => 'db' ) );" > config/resource.php 106 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 107 | - run: ./vendor/bin/phing -Ddir=ext/ai-admin-graphql setup testext 108 | 109 | "php85-mysql": 110 | docker: 111 | - image: aimeos/ci-php:8.5 112 | - image: mysql:latest 113 | environment: 114 | MYSQL_ROOT_PASSWORD: rootpw 115 | MYSQL_DATABASE: aimeos 116 | MYSQL_USER: aimeos 117 | MYSQL_PASSWORD: aimeos 118 | steps: 119 | - checkout 120 | - run: git clone --depth=50 --branch=master https://github.com/aimeos/aimeos-core ../aimeos-core 121 | - run: cd .. && mv project aimeos-core/ext/ai-admin-graphql && mv aimeos-core project && cd project 122 | - restore_cache: 123 | keys: 124 | - php85-{{ checksum "composer.json" }} 125 | - run: composer req --no-update webonyx/graphql-php 126 | - run: composer update -n --prefer-dist 127 | - save_cache: 128 | key: php85-{{ checksum "composer.json" }} 129 | paths: [./vendor] 130 | - run: echo " array( 'adapter' => 'mysql', 'host' => '127.0.0.1', 'port' => 3306, 'database' => 'aimeos', 'username' => 'aimeos', 'password' => 'aimeos', 'limit' => 2, 'opt-persistent' => false, 'stmt' => array( \"SET SESSIOn sort_buffer_size=2097144; SET NAMES 'utf8'; SET SESSION sql_mode='ANSI'\" ) ), 'fs' => array( 'adapter' => 'Standard', 'basedir' => '.' ), 'mq' => array( 'adapter' => 'Standard', 'db' => 'db' ) );" > config/resource.php 131 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 132 | - run: ./vendor/bin/phing -Ddir=ext/ai-admin-graphql setup testext 133 | 134 | workflows: 135 | version: 2 136 | unittest: 137 | jobs: 138 | - "php81-mysql" 139 | - "php82-mysql" 140 | - "php83-mysql" 141 | - "php84-mysql" 142 | - "php85-mysql" 143 | -------------------------------------------------------------------------------- /tests/Admin/Catalog/StandardTest.php: -------------------------------------------------------------------------------- 1 | context = \TestHelper::context(); 21 | $this->context->config()->set( 'admin/graphql/debug', true ); 22 | $this->context->setView( \TestHelper::view( 'unittest', $this->context->config() ) ); 23 | } 24 | 25 | 26 | public function testFindCatalog() 27 | { 28 | $body = '{"query":"query {\n findCatalog(code: \"cafe\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 29 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 30 | 31 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 32 | 33 | $this->assertStringContainsString( '"code":"cafe"', (string) $response->getBody() ); 34 | } 35 | 36 | 37 | public function testGetCatalogPath() 38 | { 39 | $id = \Aimeos\MShop::create( $this->context, 'catalog' )->find( 'cafe' ); 40 | 41 | $body = '{"query":"query {\n getCatalogPath(id: \"' . $id . '\") {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 42 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 43 | 44 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 45 | 46 | $this->assertStringContainsString( '"code":"root"', (string) $response->getBody() ); 47 | $this->assertStringContainsString( '"code":"cafe"', (string) $response->getBody() ); 48 | } 49 | 50 | 51 | public function testGetCatalogTree() 52 | { 53 | $body = '{"query":"query {\n getCatalogTree {\n id\n code\n children {\n id\n code\n children {\n id\n code\n}\n}\n}\n}\n","variables":{},"operationName":null}'; 54 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 55 | 56 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 57 | 58 | $this->assertStringContainsString( '"code":"root"', (string) $response->getBody() ); 59 | $this->assertStringContainsString( '"code":"cafe"', (string) $response->getBody() ); 60 | } 61 | 62 | 63 | public function testGetCatalogTreeId() 64 | { 65 | $id = \Aimeos\MShop::create( $this->context, 'catalog' )->find( 'categories' ); 66 | 67 | $body = '{"query":"query {\n getCatalogTree(id: \"' . $id . '\") {\n id\n code\n children {\n id\n code\n }\n}\n}\n","variables":{},"operationName":null}'; 68 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 69 | 70 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 71 | 72 | $this->assertStringContainsString( '"code":"categories"', (string) $response->getBody() ); 73 | $this->assertStringContainsString( '"code":"cafe"', (string) $response->getBody() ); 74 | } 75 | 76 | 77 | public function testGetCatalogTreeLevel() 78 | { 79 | $body = '{"query":"query {\n getCatalogTree(level: 2) {\n id\n code\n children {\n id\n code\n }\n}\n}\n","variables":{},"operationName":null}'; 80 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 81 | 82 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 83 | 84 | $this->assertStringContainsString( '"code":"root"', (string) $response->getBody() ); 85 | $this->assertStringContainsString( '"code":"categories"', (string) $response->getBody() ); 86 | } 87 | 88 | 89 | public function testInsertCatalog() 90 | { 91 | $stub = $this->getMockBuilder( '\\Aimeos\\MShop\\Catalog\\Manager\\Standard' ) 92 | ->setConstructorArgs( array( $this->context ) ) 93 | ->onlyMethods( ['insert'] ) 94 | ->getMock(); 95 | 96 | $item = $stub->create( ['catalog.code' => 'test-graphql'] ); 97 | $stub->expects( $this->once() )->method( 'insert' )->willReturn( $item ); 98 | 99 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Catalog\\Manager\\Standard', $stub ); 100 | 101 | $body = '{"query":"mutation {\n insertCatalog(input: {\n code: \"test-graphql\"\n }) {\n id\n code\n }\n}\n","variables":{},"operationName":null}'; 102 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 103 | 104 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 105 | 106 | $this->assertStringContainsString( '"code":"test-graphql"', (string) $response->getBody() ); 107 | } 108 | 109 | 110 | public function testMoveCatalog() 111 | { 112 | $stub = $this->getMockBuilder( '\\Aimeos\\MShop\\Catalog\\Manager\\Standard' ) 113 | ->setConstructorArgs( array( $this->context ) ) 114 | ->onlyMethods( ['move'] ) 115 | ->getMock(); 116 | 117 | $stub->expects( $this->once() )->method( 'move' ); 118 | 119 | \Aimeos\MShop::inject( '\\Aimeos\\MShop\\Catalog\\Manager\\Standard', $stub ); 120 | 121 | $body = '{"query":"mutation {\n moveCatalog(id: \"1\", parentid: null)\n}\n","variables":{},"operationName":null}'; 122 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 123 | 124 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 125 | 126 | $this->assertStringContainsString( '"moveCatalog":"1"', (string) $response->getBody() ); 127 | } 128 | 129 | 130 | public function testSearchCatalog() 131 | { 132 | $search = addslashes( addslashes( json_encode( ['~=' => ['catalog.code' => 'c']] ) ) ); 133 | $body = '{"query":"query {\n searchCatalogs(filter: \"' . $search . '\") {\n items {\n id\n code\n }\n total\n }\n}\n","variables":{},"operationName":null}'; 134 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 135 | 136 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 137 | 138 | $this->assertStringContainsString( '"total":3', (string) $response->getBody() ); 139 | $this->assertStringContainsString( '"code":"categories"', (string) $response->getBody() ); 140 | $this->assertStringContainsString( '"code":"cafe"', (string) $response->getBody() ); 141 | $this->assertStringContainsString( '"code":"misc"', (string) $response->getBody() ); 142 | } 143 | 144 | 145 | public function testSearchCatalogTree() 146 | { 147 | $filter = ['||' => [ 148 | ['~=' => ['catalog.code' => 'c']], 149 | ['==' => ['catalog.code' => 'new']], 150 | ['==' => ['catalog.code' => 'internet']], 151 | ['==' => ['catalog.code' => 'root']] 152 | ]]; 153 | 154 | $search = addslashes( addslashes( json_encode( $filter ) ) ); 155 | $body = '{"query":"query {\n searchCatalogTree(filter: \"' . $search . '\") {\n id\n code\n children {\n id\n code\n children {\n id\n code\n}}}\n}\n","variables":{},"operationName":null}'; 156 | $request = new \Nyholm\Psr7\ServerRequest( 'POST', 'localhost', [], $body ); 157 | 158 | $response = \Aimeos\Admin\Graphql::execute( $this->context, $request ); 159 | $result = json_decode( (string) $response->getBody(), true ); 160 | 161 | $this->assertEquals( 1, count( $result['data']['searchCatalogTree'] ) ); 162 | $this->assertEquals( 'root', $result['data']['searchCatalogTree'][0]['code'] ); 163 | $this->assertEquals( 2, count( $result['data']['searchCatalogTree'][0]['children'] ) ); 164 | $this->assertEquals( 'categories', $result['data']['searchCatalogTree'][0]['children'][0]['code'] ); 165 | $this->assertEquals( 2, count( $result['data']['searchCatalogTree'][0]['children'][0]['children'] ) ); 166 | $this->assertEquals( 'cafe', $result['data']['searchCatalogTree'][0]['children'][0]['children'][0]['code'] ); 167 | $this->assertEquals( 'misc', $result['data']['searchCatalogTree'][0]['children'][0]['children'][1]['code'] ); 168 | $this->assertEquals( 'group', $result['data']['searchCatalogTree'][0]['children'][1]['code'] ); 169 | $this->assertEquals( 2, count( $result['data']['searchCatalogTree'][0]['children'][1]['children'] ) ); 170 | $this->assertEquals( 'new', $result['data']['searchCatalogTree'][0]['children'][1]['children'][0]['code'] ); 171 | $this->assertEquals( 'internet', $result['data']['searchCatalogTree'][0]['children'][1]['children'][1]['code'] ); 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Base.php: -------------------------------------------------------------------------------- 1 | context = $context; 37 | $this->registry = $registry; 38 | } 39 | 40 | 41 | /** 42 | * Checks if the user has access to the given domain and action 43 | * 44 | * @param string $domain Domain path of the manager 45 | * @param string $action Action name 46 | * @return bool True if access is allowed, false if not 47 | */ 48 | protected function access( string $domain, string $action ) : bool 49 | { 50 | $groups = $this->context->config()->get( 'admin/graphql/resource/' . $domain . '/' . $action, [] ); 51 | 52 | if( $this->context->view()->access( $groups ) === true ) { 53 | return true; 54 | } 55 | 56 | throw new \Aimeos\Admin\Graphql\Exception( 'Forbidden', 403 ); 57 | } 58 | 59 | 60 | /** 61 | * Returns a closure for aggregating items 62 | * 63 | * @param string $domain Domain path of the manager 64 | * @return \Closure Anonymous method aggregating several items 65 | */ 66 | protected function aggregateItems( string $domain ) : \Closure 67 | { 68 | return function( $root, $args, $context ) use ( $domain ) { 69 | 70 | $this->access( $domain, 'get' ); 71 | $manager = \Aimeos\MShop::create( $this->context(), $domain ); 72 | 73 | $filter = $manager->filter()->order( $args['sort'] )->slice( 0, $args['limit'] ); 74 | $filter->add( $filter->parse( json_decode( $args['filter'], true ) ) ); 75 | 76 | return $manager->aggregate( $filter, $args['key'], $args['value'], $args['type'] )->all(); 77 | }; 78 | } 79 | 80 | 81 | /** 82 | * Returns the context object 83 | * 84 | * @return \Aimeos\MShop\ContextIface Context object 85 | */ 86 | protected function context() : \Aimeos\MShop\ContextIface 87 | { 88 | return $this->context; 89 | } 90 | 91 | 92 | /** 93 | * Returns a closure for deleting items 94 | * 95 | * @param string $domain Domain path of the manager 96 | * @return \Closure Anonymous method deleting one or more items 97 | */ 98 | protected function deleteItems( string $domain ) : \Closure 99 | { 100 | return function( $root, $args, $context ) use ( $domain ) { 101 | 102 | $this->access( $domain, 'delete' ); 103 | \Aimeos\MShop::create( $this->context(), $domain )->delete( $args['id'] ); 104 | return $args['id']; 105 | }; 106 | } 107 | 108 | 109 | /** 110 | * Returns a closure for returning a single item by its ID 111 | * 112 | * @param string $domain Domain path of the manager 113 | * @return \Closure Anonymous method returning one item 114 | */ 115 | protected function getItem( string $domain ) : \Closure 116 | { 117 | return function( $root, $args, $context ) use ( $domain ) { 118 | 119 | $this->access( $domain, 'get' ); 120 | return $this->filter( \Aimeos\MShop::create( $this->context(), $domain )->get( $args['id'], $args['include'] ) ); 121 | }; 122 | } 123 | 124 | 125 | /** 126 | * Returns the item if not removed for security reasons 127 | * 128 | * @param \Aimeos\MShop\Common\Item\Iface $item Item to check 129 | * @return \Aimeos\MShop\Common\Item\Iface Item if not removed 130 | */ 131 | protected function filter( \Aimeos\MShop\Common\Item\Iface $item ) : \Aimeos\MShop\Common\Item\Iface 132 | { 133 | return $item; 134 | } 135 | 136 | 137 | /** 138 | * Returns the items if not removed for security reasons 139 | * 140 | * @param iterable $items List of items to check 141 | * @return iterable List of items not removed 142 | */ 143 | protected function filters( iterable $items ) : iterable 144 | { 145 | return $items; 146 | } 147 | 148 | 149 | /** 150 | * Returns a closure for returning a single item by its code 151 | * 152 | * @param string $domain Domain path of the manager 153 | * @return \Closure Anonymous method returning one item 154 | */ 155 | protected function findItem( string $domain ) : \Closure 156 | { 157 | return function( $root, $args, $context ) use ( $domain ) { 158 | 159 | $this->access( $domain, 'get' ); 160 | return $this->filter( \Aimeos\MShop::create( $this->context(), $domain )->find( $args['code'], $args['include'] ) ); 161 | }; 162 | } 163 | 164 | 165 | /** 166 | * Returns a closure for returning a single type item by its code 167 | * 168 | * @param string $domain Domain path of the manager 169 | * @return \Closure Anonymous method returning one item 170 | */ 171 | protected function findTypeItem( string $domain ) : \Closure 172 | { 173 | return function( $root, $args, $context ) use ( $domain ) { 174 | 175 | $this->access( $domain, 'get' ); 176 | return $this->filter( \Aimeos\MShop::create( $this->context(), $domain )->find( $args['code'], [], $args['domain'] ) ); 177 | }; 178 | } 179 | 180 | 181 | /** 182 | * Returns a closure for returning several items 183 | * 184 | * @param string $domain Domain path of the manager 185 | * @return \Closure Anonymous method returning several items 186 | */ 187 | protected function searchItems( string $domain ) : \Closure 188 | { 189 | return function( $root, $args, $context ) use ( $domain ) { 190 | 191 | $this->access( $domain, 'get' ); 192 | $manager = \Aimeos\MShop::create( $this->context(), $domain ); 193 | 194 | $filter = $manager->filter()->order( $args['sort'] )->slice( $args['offset'], $args['limit'] ); 195 | $filter->add( $filter->parse( json_decode( $args['filter'], true ) ) ); 196 | 197 | $total = 0; 198 | $items = $this->filters( $manager->search( $filter, $args['include'], $total )->toArray() ); 199 | 200 | return [ 201 | 'items' => $items, 202 | 'total' => $total 203 | ]; 204 | }; 205 | } 206 | 207 | 208 | /** 209 | * Returns a closure for saving one item 210 | * 211 | * @param string $domain Domain path of the manager 212 | * @return \Closure Anonymous method returning one item 213 | */ 214 | protected function saveItem( string $domain ) : \Closure 215 | { 216 | return function( $root, $args, $context ) use ( $domain ) { 217 | 218 | if( empty( $entry = $args['input'] ) ) { 219 | throw new \Aimeos\Admin\Graphql\Exception( 'Parameter "input" must not be empty' ); 220 | } 221 | 222 | $this->access( $domain, 'save' ); 223 | $ref = $this->getRefs( $entry, $domain ); 224 | $manager = \Aimeos\MShop::create( $this->context(), $domain ); 225 | $key = str_replace( '/', '.', $domain ) . '.id'; 226 | 227 | if( !empty( $entry[$key] ) ) { 228 | $item = $manager->get( $entry[$key], array_unique( $ref ) ); 229 | } else { 230 | $item = $manager->create(); 231 | } 232 | 233 | return $manager->save( $this->updateItem( $manager, $item, $entry ) ); 234 | }; 235 | } 236 | 237 | 238 | /** 239 | * Returns a closure for saving several items 240 | * 241 | * @param string $domain Domain path of the manager 242 | * @return \Closure Anonymous method saving several items 243 | */ 244 | protected function saveItems( string $domain ) : \Closure 245 | { 246 | return function( $root, $args, $context ) use ( $domain ) { 247 | 248 | if( empty( $entries = (array) $args['input'] ) ) { 249 | throw new \Aimeos\Admin\Graphql\Exception( 'Parameter "input" must not be empty' ); 250 | } 251 | 252 | $this->access( $domain, 'save' ); 253 | $manager = \Aimeos\MShop::create( $this->context(), $domain ); 254 | $key = str_replace( '/', '.', $domain ) . '.id'; 255 | 256 | $ids = array_filter( array_column( $entries, $key ) ); 257 | $filter = $manager->filter()->add( $key, '==', $ids )->slice( 0, count( $entries ) ); 258 | 259 | $ref = []; 260 | foreach( $entries as $entry ) { 261 | $ref = array_merge( $ref, $this->getRefs( $entry, $domain ) ); 262 | } 263 | 264 | $map = $manager->search( $filter, array_unique( $ref ) ); 265 | $items = []; 266 | 267 | foreach( $entries as $entry ) 268 | { 269 | $item = $map->get( $entry[$key] ?? null ) ?: $manager->create(); 270 | $items[] = $this->updateItem( $manager, $item, $entry ); 271 | } 272 | 273 | return $manager->save( $items ); 274 | }; 275 | } 276 | 277 | 278 | /** 279 | * Recursively collect all referenced domains 280 | * @param array $entry Entry or subentry with input data 281 | * @param string $domain Domain of subentry 282 | * @return array Array with all domains collected 283 | */ 284 | protected function getRefs( array $entry, string $domain ): array 285 | { 286 | $ref = array_keys( $entry['lists'] ?? [] ); 287 | 288 | foreach( $entry['lists'] ?? [] as $listDomain => $subentry ) 289 | { 290 | foreach( $subentry ?? [] as $subItem ) { 291 | $ref = array_merge( $ref, $this->getRefs( $subItem['item'] ?? [], $listDomain ) ); 292 | } 293 | } 294 | 295 | if( isset( $entry['property'] ) ) { 296 | $ref[] = $domain . '/property'; 297 | } 298 | 299 | if( isset( $entry['stock'] ) ) { 300 | $ref[] = 'stock'; 301 | } 302 | 303 | return $ref; 304 | } 305 | 306 | 307 | /** 308 | * Returns the types registry 309 | * 310 | * @return \Aimeos\Admin\Graphql\Registry Type registry object 311 | */ 312 | protected function types() : Registry 313 | { 314 | return $this->registry; 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Catalog/Standard.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => Type::string(), 38 | 'args' => [ 39 | ['name' => 'id', 'type' => Type::string(), 'description' => 'Item ID'], 40 | ], 41 | 'resolve' => $this->deleteItems( $domain ), 42 | ], 43 | 'deleteCatalogs' => [ 44 | 'type' => Type::listOf( Type::string() ), 45 | 'args' => [ 46 | ['name' => 'id', 'type' => Type::listOf( Type::string() ), 'description' => 'List of item IDs'], 47 | ], 48 | 'resolve' => $this->deleteItems( $domain ), 49 | ], 50 | 'saveCatalog' => [ 51 | 'type' => $this->types()->treeOutputType( $domain ), 52 | 'args' => [ 53 | ['name' => 'input', 'type' => $this->types()->inputType( $domain ), 'description' => 'Item object'], 54 | ], 55 | 'resolve' => $this->saveItem( $domain ), 56 | ], 57 | 'saveCatalogs' => [ 58 | 'type' => Type::listOf( $this->types()->treeOutputType( $domain ) ), 59 | 'args' => [ 60 | ['name' => 'input', 'type' => Type::listOf( $this->types()->inputType( $domain ) ), 'description' => 'Item objects'], 61 | ], 62 | 'resolve' => $this->saveItems( $domain ), 63 | ], 64 | 'insertCatalog' => [ 65 | 'type' => $this->types()->treeOutputType( $domain ), 66 | 'args' => [ 67 | ['name' => 'input', 'type' => Type::nonNull( $this->types()->inputType( $domain ) ), 'description' => 'Item object'], 68 | ['name' => 'parentid', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'ID of the parent category'], 69 | ['name' => 'refid', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Category ID the new item should be inserted before'], 70 | ], 71 | 'resolve' => $this->insertItem( $domain ), 72 | ], 73 | 'moveCatalog' => [ 74 | 'type' => Type::String(), 75 | 'args' => [ 76 | ['name' => 'id', 'type' => Type::nonNull( Type::string() ), 'description' => 'ID of the category to move'], 77 | ['name' => 'parentid', 'type' => Type::string(), 'description' => 'ID of the old parent category'], 78 | ['name' => 'targetid', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'ID of the new parent category'], 79 | ['name' => 'refid', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Category ID the new item should be inserted before'], 80 | ], 81 | 'resolve' => $this->moveItem( $domain ), 82 | ] 83 | ]; 84 | } 85 | 86 | 87 | /** 88 | * Returns GraphQL schema definition for the available queries 89 | * 90 | * @param string $domain Domain name of the responsible manager 91 | * @return array GraphQL query schema definition 92 | */ 93 | public function query( string $domain ) : array 94 | { 95 | return [ 96 | 'getCatalog' => [ 97 | 'type' => $this->types()->treeOutputType( $domain ), 98 | 'args' => [ 99 | ['name' => 'id', 'type' => Type::string(), 'description' => 'Unique ID'], 100 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 101 | ], 102 | 'resolve' => $this->getItem( $domain ), 103 | ], 104 | 'getCatalogPath' => [ 105 | 'type' => Type::listOf( $this->types()->treeOutputType( $domain ) ), 106 | 'args' => [ 107 | ['name' => 'id', 'type' => Type::nonNull( Type::string() ), 'description' => 'Unique category ID'], 108 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 109 | ], 110 | 'resolve' => $this->getPath( $domain ), 111 | ], 112 | 'getCatalogTree' => [ 113 | 'type' => $this->types()->treeOutputType( $domain ), 114 | 'args' => [ 115 | ['name' => 'id', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Unique category ID'], 116 | ['name' => 'level', 'type' => Type::int(), 'defaultValue' => 3, 'description' => '1 = node only, 2 = with children, 3 = whole subtree'], 117 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 118 | ], 119 | 'resolve' => $this->getTree( $domain ), 120 | ], 121 | 'findCatalog' => [ 122 | 'type' => $this->types()->treeOutputType( $domain ), 123 | 'args' => [ 124 | ['name' => 'code', 'type' => Type::nonNull( Type::string() ), 'description' => 'Unique code'], 125 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 126 | ], 127 | 'resolve' => $this->findItem( $domain ), 128 | ], 129 | 'searchCatalogs' => [ 130 | 'type' => $this->types()->searchOutputType( $domain, fn( $path ) => $this->types()->treeOutputType( $path ) ), 131 | 'args' => [ 132 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 133 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 134 | ['name' => 'sort', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Sort keys'], 135 | ['name' => 'offset', 'type' => Type::int(), 'defaultValue' => 0, 'description' => 'Slice offset'], 136 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 100, 'description' => 'Slice size'], 137 | ], 138 | 'resolve' => $this->searchItems( $domain ), 139 | ], 140 | 'searchCatalogTree' => [ 141 | 'type' => Type::listOf( $this->types()->treeOutputType( $domain ) ), 142 | 'args' => [ 143 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 144 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 145 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 100, 'description' => 'Slice size'], 146 | ], 147 | 'resolve' => $this->searchTree( $domain ), 148 | ] 149 | ]; 150 | } 151 | 152 | 153 | /** 154 | * Returns the tree of parents including the given items as leaf nodes 155 | * 156 | * @param \Aimeos\Map $items List of items (with numeric indexes) 157 | * @param array $refs List of domains to fetch in addition 158 | * @return \Aimeos\Map List of parent items 159 | */ 160 | protected function getParents( \Aimeos\Map $items, array $refs ) : \Aimeos\Map 161 | { 162 | if( ( $parentIds = $items->getParentId()->filter() )->isEmpty() ) { 163 | return $items; 164 | } 165 | 166 | $manager = $this->manager(); 167 | $filter = $manager->filter() 168 | ->add( 'catalog.id', '==', $parentIds->unique() ) 169 | ->order( ['-catalog.level', 'sort:catalog:position'] ) 170 | ->slice( 0, 0x7fffffff ); 171 | 172 | $parents = $manager->search( $filter, $refs ); 173 | $indexes = $parentIds->unique()->flip(); 174 | $itemkeys = $items->getId()->flip(); 175 | 176 | foreach( $parents as $pid => $parent ) 177 | { 178 | if( isset( $itemkeys[$pid] ) ) { 179 | $items[$itemkeys[$pid]]->addChild( $items[$indexes[$pid]] ); 180 | unset( $items[$indexes[$pid]] ); 181 | } else { 182 | $items[$indexes[$pid]] = $parent->addChild( $items[$indexes[$pid]] ); 183 | } 184 | } 185 | 186 | return $this->getParents( $items, $refs ); 187 | } 188 | 189 | 190 | /** 191 | * Returns a closure for returning the nodes from the passed ID up to the root node 192 | * 193 | * @param string $domain Domain path of the manager 194 | * @return \Closure Anonymous method returning one item 195 | */ 196 | protected function getPath( string $domain ) : \Closure 197 | { 198 | return function( $root, $args, $context ) use ( $domain ) { 199 | return $this->manager()->getPath( $args['id'], $args['include'] ); 200 | }; 201 | } 202 | 203 | 204 | /** 205 | * Returns a closure for returning the node tree 206 | * 207 | * @param string $domain Domain path of the manager 208 | * @return \Closure Anonymous method returning one item 209 | */ 210 | protected function getTree( string $domain ) : \Closure 211 | { 212 | return function( $root, $args, $context ) use ( $domain ) { 213 | return $this->manager()->getTree( $args['id'], $args['include'], $args['level'] ); 214 | }; 215 | } 216 | 217 | 218 | /** 219 | * Returns a closure for inserting a new node into the tree 220 | * 221 | * @param string $domain Domain path of the manager 222 | * @return \Closure Anonymous method returning one item 223 | */ 224 | protected function insertItem( string $domain ) : \Closure 225 | { 226 | return function( $root, $args, $context ) use ( $domain ) { 227 | 228 | if( empty( $entry = $args['input'] ) ) { 229 | throw new \Aimeos\Admin\Graphql\Exception( 'Parameter "input" must not be empty' ); 230 | } 231 | 232 | $manager = $this->manager(); 233 | $item = $this->updateItem( $manager, $manager->create(), $entry ); 234 | 235 | return $manager->insert( $item, $args['parentid'], $args['refid'] ); 236 | }; 237 | } 238 | 239 | 240 | /** 241 | * Returns the manager for the site items 242 | * 243 | * @return \Aimeos\MShop\Common\Manager\Iface Manager object 244 | */ 245 | protected function manager() : \Aimeos\MShop\Common\Manager\Iface 246 | { 247 | if( !isset( $this->manager ) ) { 248 | $this->manager = \Aimeos\MShop::create( $this->context(), 'catalog' ); 249 | } 250 | 251 | return $this->manager; 252 | } 253 | 254 | 255 | /** 256 | * Returns a closure for moving a node within the tree 257 | * 258 | * @param string $domain Domain path of the manager 259 | * @return \Closure Anonymous method returning one item 260 | */ 261 | protected function moveItem( string $domain ) : \Closure 262 | { 263 | return function( $root, $args, $context ) use ( $domain ) { 264 | $this->manager()->move( $args['id'], $args['parentid'], $args['targetid'], $args['refid'] ); 265 | return $args['id']; 266 | }; 267 | } 268 | 269 | 270 | /** 271 | * Returns a closure for searching the tree 272 | * 273 | * @param string $domain Domain path of the manager 274 | * @return \Closure Anonymous method returning one item 275 | */ 276 | protected function searchTree( string $domain ) : \Closure 277 | { 278 | return function( $root, $args, $context ) use ( $domain ) { 279 | 280 | $this->access( $domain, 'get' ); 281 | $manager = $this->manager(); 282 | 283 | $filter = $manager->filter()->order( ['-catalog.level', 'sort:catalog:position'] ); 284 | $filter->add( $filter->parse( json_decode( $args['filter'], true ) ) ); 285 | 286 | $items = $manager->search( $filter->slice( 0, $args['limit'] ), $args['include'] ); 287 | 288 | foreach( $items as $key => $item ) 289 | { 290 | if( isset( $items[$item->getParentId()] ) ) { 291 | $items[$item->getParentId()]->addChild( $item ); 292 | unset( $items[$key] ); 293 | } 294 | } 295 | 296 | return $this->getParents( $items->values(), $args['include'] ); 297 | }; 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Locale/Site/Standard.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => Type::string(), 38 | 'args' => [ 39 | ['name' => 'id', 'type' => Type::string(), 'description' => 'Item ID'], 40 | ], 41 | 'resolve' => $this->deleteItems( $domain ), 42 | ], 43 | 'delete' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 44 | 'type' => Type::listOf( Type::string() ), 45 | 'args' => [ 46 | ['name' => 'id', 'type' => Type::listOf( Type::string() ), 'description' => 'List of item IDs'], 47 | ], 48 | 'resolve' => $this->deleteItems( $domain ), 49 | ], 50 | 'insert' . str_replace( '/', '', ucwords( $domain, '/' ) ) => [ 51 | 'type' => $this->types()->siteOutputType(), 52 | 'args' => [ 53 | ['name' => 'input', 'type' => Type::nonNull( $this->types()->inputType( $domain ) ), 'description' => 'Item object'], 54 | ['name' => 'parentid', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'ID of the parent site'], 55 | ['name' => 'refid', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Site ID the new item should be inserted before'], 56 | ], 57 | 'resolve' => $this->insertItem( $domain ), 58 | ], 59 | 'move' . str_replace( '/', '', ucwords( $domain, '/' ) ) => [ 60 | 'type' => Type::String(), 61 | 'args' => [ 62 | ['name' => 'id', 'type' => Type::nonNull( Type::string() ), 'description' => 'ID of the site to move'], 63 | ['name' => 'parentid', 'type' => Type::string(), 'description' => 'ID of the old parent site'], 64 | ['name' => 'targetid', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'ID of the new parent site'], 65 | ['name' => 'refid', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Site ID the new item should be inserted before'], 66 | ], 67 | 'resolve' => $this->moveItem( $domain ), 68 | ], 69 | 'save' . str_replace( '/', '', ucwords( $domain, '/' ) ) => [ 70 | 'type' => $this->types()->siteOutputType(), 71 | 'args' => [ 72 | ['name' => 'input', 'type' => $this->types()->inputType( $domain ), 'description' => 'Item object'], 73 | ], 74 | 'resolve' => $this->saveItem( $domain ), 75 | ], 76 | 'save' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 77 | 'type' => Type::listOf( $this->types()->siteOutputType() ), 78 | 'args' => [ 79 | ['name' => 'input', 'type' => Type::listOf( $this->types()->inputType( $domain ) ), 'description' => 'Item objects'], 80 | ], 81 | 'resolve' => $this->saveItems( $domain ), 82 | ] 83 | ]; 84 | } 85 | 86 | 87 | /** 88 | * Returns GraphQL schema definition for the available queries 89 | * 90 | * @param string $domain Domain name of the responsible manager 91 | * @return array GraphQL query schema definition 92 | */ 93 | public function query( string $domain ) : array 94 | { 95 | return [ 96 | 'find' . str_replace( '/', '', ucwords( $domain, '/' ) ) => [ 97 | 'type' => $this->types()->siteOutputType(), 98 | 'args' => [ 99 | ['name' => 'code', 'type' => Type::nonNull( Type::string() ), 'description' => 'Unique code'], 100 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 101 | ], 102 | 'resolve' => $this->findItem( $domain ), 103 | ], 104 | 'get' . str_replace( '/', '', ucwords( $domain, '/' ) ) => [ 105 | 'type' => $this->types()->siteOutputType(), 106 | 'args' => [ 107 | ['name' => 'id', 'type' => Type::string(), 'description' => 'Unique ID'], 108 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 109 | ], 110 | 'resolve' => $this->getItem( $domain ), 111 | ], 112 | 'get' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 'Path' => [ 113 | 'type' => Type::listOf( $this->types()->siteOutputType() ), 114 | 'args' => [ 115 | ['name' => 'id', 'type' => Type::nonNull( Type::string() ), 'description' => 'Unique site ID'], 116 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 117 | ], 118 | 'resolve' => $this->getPath( $domain ), 119 | ], 120 | 'get' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 'Tree' => [ 121 | 'type' => $this->types()->siteOutputType(), 122 | 'args' => [ 123 | ['name' => 'id', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Unique site ID'], 124 | ['name' => 'level', 'type' => Type::int(), 'defaultValue' => 3, 'description' => '1 = node only, 2 = with children, 3 = whole subtree'], 125 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 126 | ], 127 | 'resolve' => $this->getTree( $domain ), 128 | ], 129 | 'search' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 130 | 'type' => $this->types()->searchOutputType( $domain, fn( $path ) => $this->types()->siteOutputType() ), 131 | 'args' => [ 132 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 133 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 134 | ['name' => 'sort', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Sort keys'], 135 | ['name' => 'offset', 'type' => Type::int(), 'defaultValue' => 0, 'description' => 'Slice offset'], 136 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 100, 'description' => 'Slice size'], 137 | ], 138 | 'resolve' => $this->searchItems( $domain ), 139 | ], 140 | 'search' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 'Tree' => [ 141 | 'type' => Type::listOf( $this->types()->siteOutputType() ), 142 | 'args' => [ 143 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 144 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 145 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 100, 'description' => 'Slice size'], 146 | ], 147 | 'resolve' => $this->searchTree( $domain ), 148 | ] 149 | ]; 150 | } 151 | 152 | 153 | /** 154 | * Returns the item if not removed for security reasons 155 | * 156 | * @param \Aimeos\MShop\Common\Item\Iface $item Item to check 157 | * @return \Aimeos\MShop\Common\Item\Iface Item if not removed 158 | */ 159 | protected function filter( \Aimeos\MShop\Common\Item\Iface $item ) : \Aimeos\MShop\Common\Item\Iface 160 | { 161 | $siteid = (string) $this->context()->user()?->getSiteId(); 162 | 163 | if( $item->getSiteId() && strncmp( $item->getSiteId(), $siteid, strlen( $siteid ) ) ) { 164 | throw new \Aimeos\Admin\Graphql\Exception( 'Forbidden', 403 ); 165 | } 166 | 167 | return $item; 168 | } 169 | 170 | 171 | /** 172 | * Returns the items if not removed for security reasons 173 | * 174 | * @param iterable $items List of items to check 175 | * @return iterable List of items not removed 176 | */ 177 | protected function filters( iterable $items ) : iterable 178 | { 179 | $list = []; 180 | $siteid = (string) $this->context()->user()?->getSiteId(); 181 | 182 | foreach( $items as $id => $item ) 183 | { 184 | if( !( $item->getSiteId() && strncmp( $item->getSiteId(), $siteid, strlen( $siteid ) ) ) ) { 185 | $list[$id] = $item; 186 | } 187 | } 188 | 189 | return $list; 190 | } 191 | 192 | 193 | /** 194 | * Returns the tree of parents including the given items as leaf nodes 195 | * 196 | * @param \Aimeos\Map $items List of items (with numeric indexes) 197 | * @param array $refs List of domains to fetch in addition 198 | * @return \Aimeos\Map List of parent items 199 | */ 200 | protected function getParents( \Aimeos\Map $items, array $refs ) : \Aimeos\Map 201 | { 202 | if( ( $parentIds = $items->getParentId()->filter() )->isEmpty() ) { 203 | return $items; 204 | } 205 | 206 | $manager = $this->manager(); 207 | $filter = $manager->filter() 208 | ->add( 'locale.site.siteid', '=~', (string) $this->context()->user()?->getSiteId() ) 209 | ->add( 'locale.site.id', '==', $parentIds->unique() ) 210 | ->order( ['-locale.site.level', 'sort:locale.site:position'] ) 211 | ->slice( 0, 0x7fffffff ); 212 | 213 | $parents = $manager->search( $filter, $refs ); 214 | $indexes = $parentIds->unique()->flip(); 215 | $itemkeys = $items->getId()->flip(); 216 | 217 | foreach( $parents as $pid => $parent ) 218 | { 219 | if( isset( $itemkeys[$pid] ) ) { 220 | $items[$itemkeys[$pid]]->addChild( $items[$indexes[$pid]] ); 221 | unset( $items[$indexes[$pid]] ); 222 | } else { 223 | $items[$indexes[$pid]] = $parent->addChild( $items[$indexes[$pid]] ); 224 | } 225 | } 226 | 227 | return $this->getParents( $items, $refs ); 228 | } 229 | 230 | 231 | /** 232 | * Returns a closure for returning the nodes from the passed ID up to the root node 233 | * 234 | * @param string $domain Domain path of the manager 235 | * @return \Closure Anonymous method returning one item 236 | */ 237 | protected function getPath( string $domain ) : \Closure 238 | { 239 | return function( $root, $args, $context ) use ( $domain ) { 240 | return $this->filters( $this->manager()->getPath( $args['id'], $args['include'] ) ); 241 | }; 242 | } 243 | 244 | 245 | /** 246 | * Returns a closure for returning the node tree 247 | * 248 | * @param string $domain Domain path of the manager 249 | * @return \Closure Anonymous method returning one item 250 | */ 251 | protected function getTree( string $domain ) : \Closure 252 | { 253 | return function( $root, $args, $context ) use ( $domain ) { 254 | return $this->filter( $this->manager()->getTree( $args['id'], $args['include'], $args['level'] ) ); 255 | }; 256 | } 257 | 258 | 259 | /** 260 | * Returns a closure for inserting a new node into the tree 261 | * 262 | * @param string $domain Domain path of the manager 263 | * @return \Closure Anonymous method returning one item 264 | */ 265 | protected function insertItem( string $domain ) : \Closure 266 | { 267 | return function( $root, $args, $context ) use ( $domain ) { 268 | 269 | if( empty( $entry = $args['input'] ) ) { 270 | throw new \Aimeos\Admin\Graphql\Exception( 'Parameter "input" must not be empty' ); 271 | } 272 | 273 | $this->access( $domain, 'insert' ); 274 | $manager = $this->manager(); 275 | $item = $manager->create()->fromArray( $entry, true ); 276 | 277 | return $manager->insert( $item, $args['parentid'], $args['refid'] ); 278 | }; 279 | } 280 | 281 | 282 | /** 283 | * Returns the manager for the site items 284 | * 285 | * @return \Aimeos\MShop\Common\Manager\Iface Manager object 286 | */ 287 | protected function manager() : \Aimeos\MShop\Common\Manager\Iface 288 | { 289 | if( !isset( $this->manager ) ) { 290 | $this->manager = \Aimeos\MShop::create( $this->context(), 'locale/site' ); 291 | } 292 | 293 | return $this->manager; 294 | } 295 | 296 | 297 | /** 298 | * Returns a closure for moving a node within the tree 299 | * 300 | * @param string $domain Domain path of the manager 301 | * @return \Closure Anonymous method returning one item 302 | */ 303 | protected function moveItem( string $domain ) : \Closure 304 | { 305 | return function( $root, $args, $context ) use ( $domain ) { 306 | 307 | $this->access( $domain, 'move' ); 308 | $this->manager()->move( $args['id'], $args['parentid'], $args['targetid'], $args['refid'] ); 309 | 310 | return $args['id']; 311 | }; 312 | } 313 | 314 | 315 | /** 316 | * Returns a closure for returning several items 317 | * 318 | * @param string $domain Domain path of the manager 319 | * @return \Closure Anonymous method returning several items 320 | */ 321 | protected function searchItems( string $domain ) : \Closure 322 | { 323 | return function( $root, $args, $context ) use ( $domain ) { 324 | 325 | $this->access( $domain, 'get' ); 326 | 327 | $manager = \Aimeos\MShop::create( $this->context(), $domain ); 328 | $prefix = str_replace( '/', '.', $domain ); 329 | 330 | $filter = $manager->filter()->order( $args['sort'] )->slice( $args['offset'], $args['limit'] ); 331 | $filter->add( $prefix . '.siteid', '=~', (string) $this->context()->user()?->getSiteId() ); 332 | $filter->add( $filter->parse( json_decode( $args['filter'], true ) ) ); 333 | 334 | $total = 0; 335 | $items = $manager->search( $filter, $args['include'], $total )->toArray(); 336 | 337 | return [ 338 | 'items' => $items, 339 | 'total' => $total 340 | ]; 341 | }; 342 | } 343 | 344 | 345 | /** 346 | * Returns a closure for searching the tree 347 | * 348 | * @param string $domain Domain path of the manager 349 | * @return \Closure Anonymous method returning one item 350 | */ 351 | protected function searchTree( string $domain ) : \Closure 352 | { 353 | return function( $root, $args, $context ) use ( $domain ) { 354 | 355 | $this->access( $domain, 'get' ); 356 | $manager = $this->manager(); 357 | 358 | $filter = $manager->filter()->order( ['-locale.site.level', 'sort:locale.site:position'] ); 359 | $filter->add( 'locale.site.siteid', '=~', (string) $this->context()->user()?->getSiteId() ); 360 | $filter->add( $filter->parse( json_decode( $args['filter'], true ) ) ); 361 | 362 | $items = $manager->search( $filter->slice( 0, $args['limit'] ), $args['include'] ); 363 | 364 | foreach( $items as $key => $item ) 365 | { 366 | if( isset( $items[$item->getParentId()] ) ) { 367 | $items[$item->getParentId()]->addChild( $item ); 368 | unset( $items[$key] ); 369 | } 370 | } 371 | 372 | return $this->getParents( $items->values(), $args['include'] ); 373 | }; 374 | } 375 | 376 | 377 | /** 378 | * Updates the item 379 | * 380 | * @param \Aimeos\MShop\Common\Manager\Iface $manager Manager object for the passed item 381 | * @param \Aimeos\MShop\Common\Item\AdddressRef\Iface $item Item to update 382 | * @param array $entry Associative list of key/value pairs of the item data 383 | * @return \Aimeos\MShop\Common\Item\Iface Updated item 384 | */ 385 | protected function updateItem( \Aimeos\MShop\Common\Manager\Iface $manager, 386 | \Aimeos\MShop\Common\Item\Iface $item, array $entry ) : \Aimeos\MShop\Common\Item\Iface 387 | { 388 | $siteid = (string) $this->context()->user()?->getSiteId(); 389 | 390 | if( !$siteid || !$item->getSiteId() || strncmp( $item->getSiteId(), $siteid, strlen( $siteid ) ) ){ 391 | throw new \Aimeos\Admin\Graphql\Exception( 'Forbidden', 403 ); 392 | } 393 | 394 | return $item->fromArray( $entry, true ); 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/Admin/Graphql/Order/Standard.php: -------------------------------------------------------------------------------- 1 | [ 40 | 'type' => $this->orderOutputType( $domain ), 41 | 'args' => [ 42 | ['name' => 'input', 'type' => $this->orderInputType( $domain ), 'description' => 'Item object'], 43 | ], 44 | 'resolve' => $this->saveItem( $domain ), 45 | ], 46 | 'save' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 47 | 'type' => Type::listOf( $this->orderOutputType( $domain ) ), 48 | 'args' => [ 49 | ['name' => 'input', 'type' => Type::listOf( $this->orderInputType( $domain ) ), 'description' => 'Item objects'], 50 | ], 51 | 'resolve' => $this->saveItems( $domain ), 52 | ] 53 | ]; 54 | } 55 | 56 | 57 | /** 58 | * Returns GraphQL schema definition for the available queries 59 | * 60 | * @param string $domain Domain name of the responsible manager 61 | * @return array GraphQL query schema definition 62 | */ 63 | public function query( string $domain ) : array 64 | { 65 | return [ 66 | 'aggregate' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 67 | 'type' => $this->types()->aggregateOutputType( $domain ), 68 | 'args' => [ 69 | ['name' => 'key', 'type' => Type::listOf( Type::string() ), 'description' => 'Aggregation key to group results by, e.g. ["order.status", "order.price"]'], 70 | ['name' => 'value', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Aggregate values from that column, e.g "order.price" (optional, only if type is passed)'], 71 | ['name' => 'type', 'type' => Type::string(), 'defaultValue' => null, 'description' => 'Type of aggregation like "sum" or "avg" (default: null for count)'], 72 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 73 | ['name' => 'sort', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Sort keys'], 74 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 10000, 'description' => 'Slice size'], 75 | ], 76 | 'resolve' => $this->aggregateItems( $domain ), 77 | ], 78 | 'get' . str_replace( '/', '', ucwords( $domain, '/' ) ) => [ 79 | 'type' => $this->orderOutputType( $domain ), 80 | 'args' => [ 81 | ['name' => 'id', 'type' => Type::string(), 'description' => 'Unique ID'], 82 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 83 | ], 84 | 'resolve' => $this->getItem( $domain ), 85 | ], 86 | 'search' . str_replace( '/', '', ucwords( $domain, '/' ) ) . 's' => [ 87 | 'type' => $this->types()->searchOutputType( $domain, fn( $path ) => $this->orderOutputType( $path ) ), 88 | 'args' => [ 89 | ['name' => 'filter', 'type' => Type::string(), 'defaultValue' => '{}', 'description' => 'Filter conditions'], 90 | ['name' => 'include', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Domains to include'], 91 | ['name' => 'sort', 'type' => Type::listOf( Type::string() ), 'defaultValue' => [], 'description' => 'Sort keys'], 92 | ['name' => 'offset', 'type' => Type::int(), 'defaultValue' => 0, 'description' => 'Slice offset'], 93 | ['name' => 'limit', 'type' => Type::int(), 'defaultValue' => 100, 'description' => 'Slice size'], 94 | ], 95 | 'resolve' => $this->searchItems( $domain ), 96 | ] 97 | ]; 98 | } 99 | 100 | 101 | /** 102 | * Defines the GraphQL order input type 103 | * 104 | * @param string $path Path of the domain manager 105 | * @return \GraphQL\Type\Definition\InputObjectType Input type definition 106 | */ 107 | public function orderInputType( string $path ) : InputObjectType 108 | { 109 | $name = 'orderInput'; 110 | 111 | if( isset( $this->types[$name] ) ) { 112 | return $this->types[$name]; 113 | } 114 | 115 | return $this->types[$name] = new InputObjectType( [ 116 | 'name' => $name, 117 | 'fields' => function() use ( $path ) { 118 | 119 | $manager = \Aimeos\MShop::create( $this->context(), $path ); 120 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 121 | 122 | $list['address'] = Type::listOf( $this->types()->inputType( $path . '/address' ) ); 123 | $list['product'] = Type::listOf( $this->orderProductInputType( $path . '/product' ) ); 124 | $list['service'] = Type::listOf( $this->orderServiceInputType( $path . '/service' ) ); 125 | 126 | return $list; 127 | }, 128 | 'parseValue' => function( array $values ) use ( $path ) { 129 | return $this->types()->prefix( $path, $values ); 130 | } 131 | ] ); 132 | } 133 | 134 | 135 | /** 136 | * Defines the GraphQL order product input type 137 | * 138 | * @param string $path Path of the domain manager 139 | * @return \GraphQL\Type\Definition\InputObjectType Input type definition 140 | */ 141 | public function orderProductInputType( string $path ) : InputObjectType 142 | { 143 | $name = 'orderProductInput'; 144 | 145 | if( isset( $this->types[$name] ) ) { 146 | return $this->types[$name]; 147 | } 148 | 149 | return $this->types[$name] = new InputObjectType( [ 150 | 'name' => $name, 151 | 'fields' => function() use ( $path ) { 152 | 153 | $manager = \Aimeos\MShop::create( $this->context(), $path ); 154 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 155 | 156 | $list['product'] = Type::listOf( $this->orderSubProductInputType( $path ) ); 157 | $list['attribute'] = Type::listOf( $this->types()->inputType( $path . '/attribute' ) ); 158 | 159 | return $list; 160 | }, 161 | 'parseValue' => function( array $values ) use ( $path ) { 162 | return $this->types()->prefix( $path, $values ); 163 | } 164 | ] ); 165 | } 166 | 167 | 168 | /** 169 | * Defines the GraphQL order sub-product input type 170 | * 171 | * @param string $path Path of the domain manager 172 | * @return \GraphQL\Type\Definition\InputObjectType Input type definition 173 | */ 174 | public function orderSubProductInputType( string $path ) : InputObjectType 175 | { 176 | $name = 'orderSubProductInput'; 177 | 178 | if( isset( $this->types[$name] ) ) { 179 | return $this->types[$name]; 180 | } 181 | 182 | return $this->types[$name] = new InputObjectType( [ 183 | 'name' => $name, 184 | 'fields' => function() use ( $path ) { 185 | 186 | $manager = \Aimeos\MShop::create( $this->context(), $path ); 187 | 188 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 189 | $list['attribute'] = Type::listOf( $this->types()->inputType( $path . '/attribute' ) ); 190 | 191 | return $list; 192 | }, 193 | 'parseValue' => function( array $values ) use ( $path ) { 194 | return $this->types()->prefix( $path, $values ); 195 | } 196 | ] ); 197 | } 198 | 199 | 200 | /** 201 | * Defines the GraphQL order service input type 202 | * 203 | * @param string $path Path of the domain manager 204 | * @return \GraphQL\Type\Definition\InputObjectType Input type definition 205 | */ 206 | public function orderServiceInputType( string $path ) : InputObjectType 207 | { 208 | $name = 'orderServiceInput'; 209 | 210 | if( isset( $this->types[$name] ) ) { 211 | return $this->types[$name]; 212 | } 213 | 214 | return $this->types[$name] = new InputObjectType( [ 215 | 'name' => $name, 216 | 'fields' => function() use ( $path ) { 217 | 218 | $manager = \Aimeos\MShop::create( $this->context(), $path ); 219 | 220 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 221 | $list['attribute'] = Type::listOf( $this->types()->inputType( $path . '/attribute' ) ); 222 | $list['transaction'] = Type::listOf( $this->types()->inputType( $path . '/transaction' ) ); 223 | 224 | return $list; 225 | }, 226 | 'parseValue' => function( array $values ) use ( $path ) { 227 | return $this->types()->prefix( $path, $values ); 228 | } 229 | ] ); 230 | } 231 | 232 | 233 | /** 234 | * Defines the GraphQL order output types 235 | * 236 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 237 | */ 238 | public function orderOutputType() : ObjectType 239 | { 240 | $name = 'orderOutputType'; 241 | 242 | if( isset( $this->types[$name] ) ) { 243 | return $this->types[$name]; 244 | } 245 | 246 | return $this->types[$name] = new ObjectType( [ 247 | 'name' => $name, 248 | 'fields' => function() { 249 | 250 | $manager = \Aimeos\MShop::create( $this->context(), 'order' ); 251 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 252 | 253 | $list['address'] = [ 254 | 'type' => Type::listOf( $this->orderAddressOutputType() ), 255 | 'resolve' => function( $item ) { 256 | return $item->getAddresses()->flat( 1 ); 257 | } 258 | ]; 259 | 260 | $list['coupon'] = [ 261 | 'type' => Type::listOf( $this->orderCouponOutputType() ), 262 | 'resolve' => function( $item ) { 263 | return $item->getCoupons()->keys()->all(); 264 | } 265 | ]; 266 | 267 | $list['product'] = [ 268 | 'type' => Type::listOf( $this->orderProductOutputType() ), 269 | 'resolve' => function( $item ) { 270 | return $item->getProducts(); 271 | } 272 | ]; 273 | 274 | $list['service'] = [ 275 | 'type' => Type::listOf( $this->orderServiceOutputType() ), 276 | 'resolve' => function( $item ) { 277 | return $item->getServices()->flat( 1 ); 278 | } 279 | ]; 280 | 281 | $list['status'] = [ 282 | 'type' => Type::listOf( $this->orderStatusOutputType() ), 283 | 'resolve' => function( $item ) { 284 | return $item->getStatuses()->flat( 1 ); 285 | } 286 | ]; 287 | 288 | return $list; 289 | }, 290 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Iface $item, array $args, $context, ResolveInfo $info ) { 291 | return $this->types()->resolve( $item, 'order', $info->fieldName ); 292 | } 293 | ] ); 294 | } 295 | 296 | 297 | /** 298 | * Defines the GraphQL order address output types 299 | * 300 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 301 | */ 302 | public function orderAddressOutputType() : ObjectType 303 | { 304 | $name = 'orderAddressOutput'; 305 | 306 | if( isset( $this->types[$name] ) ) { 307 | return $this->types[$name]; 308 | } 309 | 310 | return $this->types[$name] = new ObjectType( [ 311 | 'name' => $name, 312 | 'fields' => function() { 313 | $manager = \Aimeos\MShop::create( $this->context(), 'order/address' ); 314 | return $this->types()->fields( $manager->getSearchAttributes( false ) ); 315 | }, 316 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Address\Iface $item, array $args, $context, ResolveInfo $info ) { 317 | return $this->types()->resolve( $item, 'order/address', $info->fieldName ); 318 | } 319 | ] ); 320 | } 321 | 322 | 323 | /** 324 | * Defines the GraphQL order coupon output types 325 | * 326 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 327 | */ 328 | public function orderCouponOutputType() : ObjectType 329 | { 330 | $name = 'orderCouponOutput'; 331 | 332 | if( isset( $this->types[$name] ) ) { 333 | return $this->types[$name]; 334 | } 335 | 336 | return $this->types[$name] = new ObjectType( [ 337 | 'name' => $name, 338 | 'fields' => function() { 339 | return [ 340 | 'code' => [ 341 | 'name' => 'code', 342 | 'description' => 'Coupon codes', 343 | 'type' => Type::String(), 344 | ], 345 | ]; 346 | }, 347 | 'resolveField' => function( $codes, array $args, $context, ResolveInfo $info ) { 348 | return (string) $codes; 349 | } 350 | ] ); 351 | } 352 | 353 | 354 | /** 355 | * Defines the GraphQL order product output types 356 | * 357 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 358 | */ 359 | public function orderProductOutputType() : ObjectType 360 | { 361 | $name = 'orderProductOutput'; 362 | 363 | if( isset( $this->types[$name] ) ) { 364 | return $this->types[$name]; 365 | } 366 | 367 | return $this->types[$name] = new ObjectType( [ 368 | 'name' => $name, 369 | 'fields' => function() { 370 | $manager = \Aimeos\MShop::create( $this->context(), 'order/product' ); 371 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 372 | 373 | $list['product'] = [ 374 | 'type' => Type::listOf( $this->orderSubProductOutputType() ), 375 | 'resolve' => function( $item ) { 376 | return $item->getProducts(); 377 | } 378 | ]; 379 | 380 | $list['attribute'] = [ 381 | 'type' => Type::listOf( $this->orderProductAttributeOutputType() ), 382 | 'args' => [ 383 | 'type' => Type::String(), 384 | ], 385 | 'resolve' => function( $item, $args ) { 386 | return $item->getAttributeItems( $args['type'] ?? null ); 387 | } 388 | ]; 389 | 390 | return $list; 391 | }, 392 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Product\Iface $item, array $args, $context, ResolveInfo $info ) { 393 | return $this->types()->resolve( $item, 'order/product', $info->fieldName ); 394 | } 395 | ] ); 396 | } 397 | 398 | 399 | /** 400 | * Defines the GraphQL order sub-product output types 401 | * 402 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 403 | */ 404 | public function orderSubProductOutputType() : ObjectType 405 | { 406 | $name = 'orderSubProductOutput'; 407 | 408 | if( isset( $this->types[$name] ) ) { 409 | return $this->types[$name]; 410 | } 411 | 412 | return $this->types[$name] = new ObjectType( [ 413 | 'name' => $name, 414 | 'fields' => function() { 415 | $manager = \Aimeos\MShop::create( $this->context(), 'order/product' ); 416 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 417 | 418 | $list['attribute'] = [ 419 | 'type' => Type::listOf( $this->orderProductAttributeOutputType() ), 420 | 'args' => [ 421 | 'type' => Type::String(), 422 | ], 423 | 'resolve' => function( $item, $args ) { 424 | return $item->getAttributeItems( $args['type'] ?? null ); 425 | } 426 | ]; 427 | 428 | return $list; 429 | }, 430 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Product\Iface $item, array $args, $context, ResolveInfo $info ) { 431 | return $this->types()->resolve( $item, 'order/product', $info->fieldName ); 432 | } 433 | ] ); 434 | } 435 | 436 | 437 | /** 438 | * Defines the GraphQL order product attribute output types 439 | * 440 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 441 | */ 442 | public function orderProductAttributeOutputType() : ObjectType 443 | { 444 | $name = 'orderProductAttributeOutput'; 445 | 446 | if( isset( $this->types[$name] ) ) { 447 | return $this->types[$name]; 448 | } 449 | 450 | return $this->types[$name] = new ObjectType( [ 451 | 'name' => $name, 452 | 'fields' => function() { 453 | $manager = \Aimeos\MShop::create( $this->context(), 'order/product/attribute' ); 454 | return $this->types()->fields( $manager->getSearchAttributes( false ) ); 455 | }, 456 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Product\Attribute\Iface $item, array $args, $context, ResolveInfo $info ) { 457 | return $this->types()->resolve( $item, 'order/product/attribute', $info->fieldName ); 458 | } 459 | ] ); 460 | } 461 | 462 | 463 | /** 464 | * Defines the GraphQL order service output types 465 | * 466 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 467 | */ 468 | public function orderServiceOutputType() : ObjectType 469 | { 470 | $name = 'orderServiceOutput'; 471 | 472 | if( isset( $this->types[$name] ) ) { 473 | return $this->types[$name]; 474 | } 475 | 476 | return $this->types[$name] = new ObjectType( [ 477 | 'name' => $name, 478 | 'fields' => function() { 479 | $manager = \Aimeos\MShop::create( $this->context(), 'order/service' ); 480 | $list = $this->types()->fields( $manager->getSearchAttributes( false ) ); 481 | 482 | $list['attribute'] = [ 483 | 'type' => Type::listOf( $this->orderServiceAttributeOutputType() ), 484 | 'args' => [ 485 | 'type' => Type::String(), 486 | ], 487 | 'resolve' => function( $item, $args ) { 488 | return $item->getAttributeItems( $args['type'] ?? null ); 489 | } 490 | ]; 491 | 492 | $list['transaction'] = [ 493 | 'type' => Type::listOf( $this->orderServiceTransactionOutputType() ), 494 | 'args' => [ 495 | 'type' => Type::String(), 496 | ], 497 | 'resolve' => function( $item, $args ) { 498 | return $item->getTransactions( $args['type'] ?? null ); 499 | } 500 | ]; 501 | 502 | return $list; 503 | }, 504 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Service\Iface $item, array $args, $context, ResolveInfo $info ) { 505 | return $this->types()->resolve( $item, 'order/service', $info->fieldName ); 506 | } 507 | ] ); 508 | } 509 | 510 | 511 | /** 512 | * Defines the GraphQL order service attribute output types 513 | * 514 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 515 | */ 516 | public function orderServiceAttributeOutputType() : ObjectType 517 | { 518 | $name = 'orderServiceAttributeOutput'; 519 | 520 | if( isset( $this->types[$name] ) ) { 521 | return $this->types[$name]; 522 | } 523 | 524 | return $this->types[$name] = new ObjectType( [ 525 | 'name' => $name, 526 | 'fields' => function() { 527 | $manager = \Aimeos\MShop::create( $this->context(), 'order/service/attribute' ); 528 | return $this->types()->fields( $manager->getSearchAttributes( false ) ); 529 | }, 530 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Service\Attribute\Iface $item, array $args, $context, ResolveInfo $info ) { 531 | return $this->types()->resolve( $item, 'order/service/attribute', $info->fieldName ); 532 | } 533 | ] ); 534 | } 535 | 536 | 537 | /** 538 | * Defines the GraphQL order service transaction output types 539 | * 540 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 541 | */ 542 | public function orderServiceTransactionOutputType() : ObjectType 543 | { 544 | $name = 'orderServiceTransactionOutput'; 545 | 546 | if( isset( $this->types[$name] ) ) { 547 | return $this->types[$name]; 548 | } 549 | 550 | return $this->types[$name] = new ObjectType( [ 551 | 'name' => $name, 552 | 'fields' => function() { 553 | $manager = \Aimeos\MShop::create( $this->context(), 'order/service/transaction' ); 554 | return $this->types()->fields( $manager->getSearchAttributes( false ) ); 555 | }, 556 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Service\Transaction\Iface $item, array $args, $context, ResolveInfo $info ) { 557 | return $this->types()->resolve( $item, 'order/service/transaction', $info->fieldName ); 558 | } 559 | ] ); 560 | } 561 | 562 | 563 | /** 564 | * Defines the GraphQL order status output types 565 | * 566 | * @return \GraphQL\Type\Definition\ObjectType Output type definition 567 | */ 568 | public function orderStatusOutputType() : ObjectType 569 | { 570 | $name = 'orderStatusOutput'; 571 | 572 | if( isset( $this->types[$name] ) ) { 573 | return $this->types[$name]; 574 | } 575 | 576 | return $this->types[$name] = new ObjectType( [ 577 | 'name' => $name, 578 | 'fields' => function() { 579 | $manager = \Aimeos\MShop::create( $this->context(), 'order/status' ); 580 | return $this->types()->fields( $manager->getSearchAttributes( false ) ); 581 | }, 582 | 'resolveField' => function( \Aimeos\MShop\Order\Item\Status\Iface $item, array $args, $context, ResolveInfo $info ) { 583 | return $this->types()->resolve( $item, 'order/status', $info->fieldName ); 584 | } 585 | ] ); 586 | } 587 | } 588 | --------------------------------------------------------------------------------