diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index fa1f0697a0..15d347e241 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -1,29 +1,16 @@ name = $name; - } - parent::__construct(); - } - - public function parseValue($value) - { - return $this->identity($value); - } - - public function serialize($value) - { - return $this->identity($value); - } - - public function parseLiteral(Node $valueNode, ?array $variables = null) - { - switch ($valueNode) { - case ($valueNode instanceof StringValueNode): - case ($valueNode instanceof BooleanValueNode): - return $valueNode->value; - case ($valueNode instanceof IntValueNode): - case ($valueNode instanceof FloatValueNode): - return floatval($valueNode->value); - case ($valueNode instanceof ObjectValueNode): { - $value = []; - foreach ($valueNode->fields as $field) { - $value[$field->name->value] = $this->parseLiteral($field->value); - } - return $value; - } - case ($valueNode instanceof ListValueNode): - return array_map([$this, 'parseLiteral'], $valueNode->values); - default: - return null; - } - - } - - private function identity($value) - { - return $value; - } - -} - - -global $typeMapping; - -global $jsonParser; -$jsonParser = new JsonType(); - -$typeMapping = [ - Model::TYPE_BOOLEAN => Type::boolean(), - Model::TYPE_STRING => Type::string(), - Model::TYPE_INTEGER => Type::int(), - Model::TYPE_FLOAT => Type::float(), - - // Outliers - Model::TYPE_JSON => $jsonParser, - Response::MODEL_ANY => $jsonParser, - Response::MODEL_NONE => Type::string(), -]; - - -function createTypeMapping(Model $model, Response $response) { - - global $typeMapping; - - /* - If the map already contains the type, end the recursion - and return. - */ - if (isset($typeMapping[$model->getType()])) return; - - $rules = $model->getRules(); - $name = $model->getType(); - $fields = []; - $type = null; - /* - Iterate through all the rules in the response model. Each rule is of the form - [ - [KEY 1] => [ - 'type' => A string from Appwrite/Utopia/Response - 'description' => A description of the type - 'default' => A default value for this type - 'example' => An example of this type - 'require' => a boolean representing whether this field is required - 'array' => a boolean representing whether this field is an array - ], - [KEY 2] => [ - ], - [KEY 3] => [ - ] ..... - ] - */ - foreach ($rules as $key => $props) { - /* - If there are any field names containing characters other than a-z, A-Z, 0-9, _ , - we need to remove all those characters. Currently Appwrite's Response model has only the - $ sign which is prohibited. So we're only replacing that. We need to replace this with a regex - based approach. - */ - $keyWithoutSpecialChars = str_replace('$', '', $key); - if (isset($typeMapping[$props['type']])) { - $type = $typeMapping[$props['type']]; - } else { - try { - $complexModel = $response->getModel($props['type']); - createTypeMapping($complexModel, $response); - $type = $typeMapping[$props['type']]; - } catch (Exception $e) { - var_dump("Could Not find model for : {$props['type']}"); - } - } - - /* If any of the rules is a list, - Wrap the base type with a listOf Type - */ - if ($props['array']) { - $type = Type::listOf($type); - } - - $fields[$keyWithoutSpecialChars] = [ - 'type' => $type, - 'description' => $props['description'], - 'resolve' => function ($object, $args, $context, $info) use ($key, $type) { - var_dump("************* RESOLVING FIELD {$info->fieldName} *************"); - // var_dump($info->returnType->getWrappedType()); - - var_dump("isListType : ", $info->returnType instanceof ListOfType); - var_dump("isBuiltinType : ", Type::isBuiltInType($info->returnType)); - var_dump("isCompositeType : ", Type::isCompositeType($info->returnType)); - var_dump("isLeafType : ", Type::isLeafType($info->returnType)); - var_dump("isOutputType : ", Type::isOutputType($info->returnType)); - - var_dump("PHP Type of object: " . gettype($object[$key])); - // switch(gettype($object[$key])) { - // case 'array': - // $isAssoc = count(array_filter(array_keys($object[$key]), 'is_string')) > 0 ; - // if ($isAssoc) { - // return json_encode($object[$key]); - // } else { - // return array_map('json_encode', $object[$key]); - // } - // case 'object': - // return json_encode($object[$key]); - // default: - // return $object[$key]; - // } - - return $object[$key]; - } - ]; - } - - $objectType = [ - 'name' => $name, - 'fields' => $fields - ]; - - $typeMapping[$name] = new ObjectType($objectType); -} - - -function getArgType($validator, bool $required, $utopia, $injections) { - $validator = (\is_callable($validator)) ? call_user_func_array($validator, $utopia->getResources($injections)) : $validator; - $type = []; - switch ((!empty($validator)) ? \get_class($validator) : '') { - case 'Utopia\Validator\Text': - $type = Type::string(); - break; - case 'Utopia\Validator\Boolean': - $type = Type::boolean(); - break; - case 'Appwrite\Database\Validator\UID': - $type = Type::string(); - break; - case 'Utopia\Validator\Email': - $type = Type::string(); - break; - case 'Utopia\Validator\URL': - $type = Type::string(); - break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': - $type = Type::string(); - break; - case 'Appwrite\Storage\Validator\File': - $type = Type::string(); - case 'Utopia\Validator\ArrayList': - $type = Type::listOf(Type::string()); - break; - case 'Appwrite\Auth\Validator\Password': - $type = Type::string(); - break; - case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ - $type = Type::int(); - break; - case 'Utopia\Validator\Numeric': - $type = Type::int(); - break; - case 'Utopia\Validator\Length': - $type = Type::string(); - break; - case 'Utopia\Validator\Host': - $type = Type::string(); - break; - case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ - $type = Type::string(); - break; - default: - $type = Type::string(); - break; - } - - if ($required) { - $type = Type::nonNull($type); - } - - return $type; -} - -function getArgs(array $params, $utopia) { - $args = []; - foreach ($params as $key => $value) { - $args[$key] = [ - 'type' => getArgType($value['validator'],!$value['optional'], $utopia, $value['injections']), - 'description' => $value['description'], - 'defaultValue' => $value['default'] - ]; - } - return $args; -} - -function isModel($response, Model $model) { - - foreach ($model->getRules() as $key => $rule) { - if (!isset($response[$key])) { - return false; - } - } - return true; -} - class MySafeException extends \Exception implements ClientAware { public function isClientSafe() @@ -323,150 +53,23 @@ class MySafeException extends \Exception implements ClientAware } } -function buildSchema($utopia, $response) { - $start = microtime(true); - - global $typeMapping; - $queryFields = []; - $mutationFields = []; - foreach($utopia->getRoutes() as $method => $routes ){ - foreach($routes as $route) { - $namespace = $route->getLabel('sdk.namespace', ''); - - if ($namespace == 'database' || true) { - $methodName = $namespace.'_'.$route->getLabel('sdk.method', ''); - $responseModelName = $route->getLabel('sdk.response.model', ""); - // var_dump("******************************************"); - // var_dump("Processing route : ${method} : {$route->getURL()}"); - // var_dump("Model Name : ${responseModelName}"); - if ( $responseModelName !== "" && $responseModelName !== Response::MODEL_NONE ) { - $responseModel = $response->getModel($responseModelName); - createTypeMapping($responseModel, $response); - $type = $typeMapping[$responseModel->getType()]; - // var_dump("Type Created : ${type}"); - $args = getArgs($route->getParams(), $utopia); - // var_dump("Args Generated :"); - // var_dump($args); - - $field = [ - 'type' => $type, - 'description' => $route->getDesc(), - 'args' => $args, - 'resolve' => function ($type, $args, $context, $info) use (&$utopia, $route, $response) { - var_dump("************* REACHED RESOLVE FOR {$info->fieldName} *****************"); - // var_dump($route); - - // var_dump("************* CONTEXT *****************"); - // var_dump($context); - - - var_dump("********************** ARGS *******************"); - var_dump($args); - - $utopia->setRoute($route); - $utopia->execute($route, $args); - - var_dump("**************** OUTPUT ************"); - // var_dump($response->getPayload()); - $result = $response->getPayload(); - - if (isModel($result, $response->getModel(Response::MODEL_ERROR)) || isModel($result, $response->getModel(Response::MODEL_ERROR_DEV))) { - throw new MySafeException($result['message'], $result['code']); - } - - return $result; - } - ]; - - if ($method == 'GET') { - $queryFields[$methodName] = $field; - } else if ($method == 'POST' || $method == 'PUT' || $method == 'PATCH' || $method == 'DELETE') { - $mutationFields[$methodName] = $field; - } - - // var_dump("Processed route : ${method} : {$route->getURL()}"); - } else { - // var_dump("Skipping route : {$route->getURL()}"); - } - } - } - } - - ksort($queryFields); - ksort($mutationFields); - - $queryType = new ObjectType([ - 'name' => 'Query', - 'description' => 'The root of all your queries', - 'fields' => $queryFields - ]); - - $mutationType = new ObjectType([ - 'name' => 'Mutation', - 'description' => 'The root of all your mutations', - 'fields' => $mutationFields - ]); - - $schema = new Schema([ - 'query' => $queryType, - 'mutation' => $mutationType - ]); - - $time_elapsed_secs = microtime(true) - $start; - - var_dump("Time Taken To Build Schema : ${time_elapsed_secs}s"); - - return $schema; -} - - App::post('/v1/graphql') ->desc('GraphQL Endpoint') ->label('scope', 'graphql') ->inject('request') ->inject('response') - ->inject('utopia') - ->inject('user') - ->inject('project') + ->inject('schema') ->middleware(true) - ->action(function ($request, $response, $utopia, $user, $project) { + ->action(function ($request, $response, $schema) { - - // Generate the Schema of the server on startup. - // Use the routes from utopia and get the params then construct the queries and mutations. - $schema = buildSchema($utopia, $response, $request); $query = $request->getPayload('query', ''); $variables = $request->getPayload('variables', null); $response->setContentType(Response::CONTENT_TYPE_NULL); - $callable = function ($objectValue, $args, $context, $info) { - - var_dump("Entering Custom Field Resolver. \n"); - var_dump("************* RESOLVING FIELD {$info->fieldName} *************"); - $fieldName = $info->fieldName; - $property = null; - - if (is_array($objectValue) || $objectValue instanceof \ArrayAccess) { - if (isset($objectValue[$fieldName])) { - $property = $objectValue[$fieldName]; - } - } elseif (is_object($objectValue)) { - if (isset($objectValue->{$fieldName})) { - $property = $objectValue->{$fieldName}; - } - } - - return $property instanceof Closure - ? $property($objectValue, $args, $context, $info) - : $property; - }; - try { $rootValue = []; - $result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variables, null, $callable); + $result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variables); $output = $result->toArray(); - // var_dump("********** OUTPUT *********"); - // var_dump($output); } catch (\Exception $error) { $output = [ 'errors' => [ diff --git a/app/init.php b/app/init.php index f469933b39..14e1f802a1 100644 --- a/app/init.php +++ b/app/init.php @@ -21,6 +21,7 @@ use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; use Appwrite\Extend\PDO; +use Appwrite\GraphQL\GraphQLBuilder; use Appwrite\OpenSSL\OpenSSL; use Utopia\App; use Utopia\View; @@ -285,10 +286,6 @@ Locale::setLanguage('zh-tw', include __DIR__.'/config/locale/translations/zh-tw. // Runtime Execution -App::setResource('routeToScopeMapping', function($register) { - return new Event(Event::MAILS_QUEUE_NAME, Event::MAILS_CLASS_NAME); -}, ['register']); - App::setResource('register', function() use ($register) { return $register; }); @@ -490,3 +487,24 @@ App::setResource('geodb', function($register) { /** @var Utopia\Registry\Registry $register */ return $register->get('geodb'); }, ['register']); + +App::setResource('schema', function($utopia, $response, $request, $register) { + + $schema = null; + try { + /* + Try to get the schema from the register. + If there is no schema, an exception will be thrown + */ + var_dump('[INFO] Getting Schema from register..'); + $schema = $register->get('_schema'); + } catch (Exception $e) { + var_dump('[INFO] Exception, Schema not present. Generating Schema'); + $schema = GraphQLBuilder::buildSchema($utopia, $response, $request); + $register->set('_schema', function () use ($schema){ // Register cache connection + return $schema; + }); + } + + return $schema; +}, ['utopia', 'response', 'request', 'register']); diff --git a/src/Appwrite/GraphQL/GraphQLBuilder.php b/src/Appwrite/GraphQL/GraphQLBuilder.php new file mode 100644 index 0000000000..c8c56a4074 --- /dev/null +++ b/src/Appwrite/GraphQL/GraphQLBuilder.php @@ -0,0 +1,296 @@ + Type::boolean(), + Model::TYPE_STRING => Type::string(), + Model::TYPE_INTEGER => Type::int(), + Model::TYPE_FLOAT => Type::float(), + Response::MODEL_NONE => Type::string(), + Model::TYPE_JSON => self::$jsonParser, + Response::MODEL_ANY => self::$jsonParser, + ]; + } + + static function createTypeMapping(Model $model, Response $response) { + + /* + If the map already contains the type, end the recursion + and return. + */ + if (isset(self::$typeMapping[$model->getType()])) return; + + $rules = $model->getRules(); + $name = $model->getType(); + $fields = []; + $type = null; + /* + Iterate through all the rules in the response model. Each rule is of the form + [ + [KEY 1] => [ + 'type' => A string from Appwrite/Utopia/Response + 'description' => A description of the type + 'default' => A default value for this type + 'example' => An example of this type + 'require' => a boolean representing whether this field is required + 'array' => a boolean representing whether this field is an array + ], + [KEY 2] => [ + ], + [KEY 3] => [ + ] ..... + ] + */ + foreach ($rules as $key => $props) { + /* + If there are any field names containing characters other than a-z, A-Z, 0-9, _ , + we need to remove all those characters. Currently Appwrite's Response model has only the + $ sign which is prohibited. So we're only replacing that. We need to replace this with a regex + based approach. + */ + $keyWithoutSpecialChars = str_replace('$', '', $key); + if (isset(self::$typeMapping[$props['type']])) { + $type = self::$typeMapping[$props['type']]; + } else { + try { + $complexModel = $response->getModel($props['type']); + self::createTypeMapping($complexModel, $response); + $type = self::$typeMapping[$props['type']]; + } catch (Exception $e) { + var_dump("Could Not find model for : {$props['type']}"); + } + } + + /* + If any of the rules is a list, + Wrap the base type with a listOf Type + */ + if ($props['array']) { + $type = Type::listOf($type); + } + + $fields[$keyWithoutSpecialChars] = [ + 'type' => $type, + 'description' => $props['description'], + 'resolve' => function ($object, $args, $context, $info) use ($key, $type) { + + // var_dump("************* RESOLVING FIELD {$info->fieldName} *************"); + // var_dump($info->returnType->getWrappedType()); + // var_dump("isListType : ", $info->returnType instanceof ListOfType); + // var_dump("isCompositeType : ", Type::isCompositeType($info->returnType)); + // var_dump("isBuiltinType : ", Type::isBuiltInType($info->returnType)); + // var_dump("isLeafType : ", Type::isLeafType($info->returnType)); + // var_dump("isOutputType : ", Type::isOutputType($info->returnType)); + // var_dump("PHP Type of object: " . gettype($object[$key])); + + return $object[$key]; + } + ]; + } + + $objectType = [ + 'name' => $name, + 'fields' => $fields + ]; + + self::$typeMapping[$name] = new ObjectType($objectType); + } + + private static function getArgType($validator, bool $required, $utopia, $injections) { + $validator = (\is_callable($validator)) ? call_user_func_array($validator, $utopia->getResources($injections)) : $validator; + $type = []; + switch ((!empty($validator)) ? \get_class($validator) : '') { + case 'Utopia\Validator\Text': + $type = Type::string(); + break; + case 'Utopia\Validator\Boolean': + $type = Type::boolean(); + break; + case 'Appwrite\Database\Validator\UID': + $type = Type::string(); + break; + case 'Utopia\Validator\Email': + $type = Type::string(); + break; + case 'Utopia\Validator\URL': + $type = Type::string(); + break; + case 'Utopia\Validator\JSON': + case 'Utopia\Validator\Mock': + case 'Utopia\Validator\Assoc': + $type = Type::string(); + break; + case 'Appwrite\Storage\Validator\File': + $type = Type::string(); + case 'Utopia\Validator\ArrayList': + $type = Type::listOf(Type::string()); + break; + case 'Appwrite\Auth\Validator\Password': + $type = Type::string(); + break; + case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ + $type = Type::int(); + break; + case 'Utopia\Validator\Numeric': + $type = Type::int(); + break; + case 'Utopia\Validator\Length': + $type = Type::string(); + break; + case 'Utopia\Validator\Host': + $type = Type::string(); + break; + case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ + $type = Type::string(); + break; + default: + $type = Type::string(); + break; + } + + if ($required) { + $type = Type::nonNull($type); + } + + return $type; + } + + private static function getArgs(array $params, $utopia) { + $args = []; + foreach ($params as $key => $value) { + $args[$key] = [ + 'type' => self::getArgType($value['validator'],!$value['optional'], $utopia, $value['injections']), + 'description' => $value['description'], + 'defaultValue' => $value['default'] + ]; + } + return $args; + } + + private static function isModel($response, Model $model) { + + foreach ($model->getRules() as $key => $rule) { + if (!isset($response[$key])) { + return false; + } + } + return true; + } + + public static function buildSchema($utopia, $response) { + + self::init(); + + var_dump("[INFO] Building GraphQL Schema..."); + $start = microtime(true); + + $queryFields = []; + $mutationFields = []; + foreach($utopia->getRoutes() as $method => $routes ){ + foreach($routes as $route) { + $namespace = $route->getLabel('sdk.namespace', ''); + + if ($namespace == 'database' || true) { + $methodName = $namespace.'_'.$route->getLabel('sdk.method', ''); + $responseModelName = $route->getLabel('sdk.response.model', ""); + // var_dump("******************************************"); + // var_dump("Processing route : ${method} : {$route->getURL()}"); + // var_dump("Model Name : ${responseModelName}"); + if ( $responseModelName !== "" && $responseModelName !== Response::MODEL_NONE ) { + $responseModel = $response->getModel($responseModelName); + self::createTypeMapping($responseModel, $response); + $type = self::$typeMapping[$responseModel->getType()]; + // var_dump("Type Created : ${type}"); + $args = self::getArgs($route->getParams(), $utopia); + // var_dump("Args Generated :"); + // var_dump($args); + + $field = [ + 'type' => $type, + 'description' => $route->getDesc(), + 'args' => $args, + 'resolve' => function ($type, $args, $context, $info) use (&$utopia, $route, $response) { + // var_dump("************* REACHED RESOLVE FOR {$info->fieldName} *****************"); + // var_dump($route); + // var_dump("************* CONTEXT *****************"); + // var_dump($context); + // var_dump("********************** ARGS *******************"); + // var_dump($args); + + $utopia->setRoute($route); + $utopia->execute($route, $args); + + // var_dump("**************** OUTPUT ************"); + // var_dump($response->getPayload()); + + $result = $response->getPayload(); + + if (self::isModel($result, $response->getModel(Response::MODEL_ERROR)) || self::isModel($result, $response->getModel(Response::MODEL_ERROR_DEV))) { + throw new MySafeException($result['message'], $result['code']); + } + + return $result; + } + ]; + + if ($method == 'GET') { + $queryFields[$methodName] = $field; + } else if ($method == 'POST' || $method == 'PUT' || $method == 'PATCH' || $method == 'DELETE') { + $mutationFields[$methodName] = $field; + } + + // var_dump("Processed route : ${method} : {$route->getURL()}"); + } else { + // var_dump("Skipping route : {$route->getURL()}"); + } + } + } + } + + ksort($queryFields); + ksort($mutationFields); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'description' => 'The root of all your queries', + 'fields' => $queryFields + ]); + + $mutationType = new ObjectType([ + 'name' => 'Mutation', + 'description' => 'The root of all your mutations', + 'fields' => $mutationFields + ]); + + $schema = new Schema([ + 'query' => $queryType, + 'mutation' => $mutationType + ]); + + + $time_elapsed_secs = microtime(true) - $start; + var_dump("[INFO] Time Taken To Build Schema : ${time_elapsed_secs}s"); + + return $schema; + } +} diff --git a/src/Appwrite/GraphQL/Types/JsonType.php b/src/Appwrite/GraphQL/Types/JsonType.php new file mode 100644 index 0000000000..c2994c1f18 --- /dev/null +++ b/src/Appwrite/GraphQL/Types/JsonType.php @@ -0,0 +1,67 @@ +name = $name; + } + parent::__construct(); + } + + public function parseValue($value) + { + return $this->identity($value); + } + + public function serialize($value) + { + return $this->identity($value); + } + + public function parseLiteral(Node $valueNode, ?array $variables = null) + { + switch ($valueNode) { + case ($valueNode instanceof StringValueNode): + case ($valueNode instanceof BooleanValueNode): + return $valueNode->value; + case ($valueNode instanceof IntValueNode): + case ($valueNode instanceof FloatValueNode): + return floatval($valueNode->value); + case ($valueNode instanceof ObjectValueNode): { + $value = []; + foreach ($valueNode->fields as $field) { + $value[$field->name->value] = $this->parseLiteral($field->value); + } + return $value; + } + case ($valueNode instanceof ListValueNode): + return array_map([$this, 'parseLiteral'], $valueNode->values); + default: + return null; + } + + } + + private function identity($value) + { + return $value; + } +}