Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW: Schema initialise task #433

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions _config/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ SilverStripe\GraphQL\Dev\DevelopmentAdmin:
controller: SilverStripe\GraphQL\Dev\Build
links:
build: Build the GraphQL schema
init:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any docs that links to vendor/bin/sake dev/graphql/init? Right now it's very hidden

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

controller: SilverStripe\GraphQL\Dev\Initialise
links:
init: Initialise a GraphQL schema in your project directory (CLI only)
250 changes: 250 additions & 0 deletions src/Dev/Initialise.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<?php

namespace SilverStripe\GraphQL\Dev;

use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\Core\Path;
use SilverStripe\GraphQL\Schema\Schema;
use SilverStripe\ORM\Connect\NullDatabaseException;

/**
* A task that initialises a schema with boilerplate config and files.
*/
class Initialise extends Controller
{
/**
* @var string[]
*/
private static $url_handlers = [
'' => 'initialise'
];

/**
* @var string[]
*/
private static $allowed_actions = [
'initialise'
];

/**
* @var string
*/
private $appNamespace;

/**
* @var string
*/
private $schemaName = 'default';

/**
* @var string
*/
private $graphqlConfigDir = '_graphql';

/**
* @var string
*/
private $graphqlCodeDir = 'GraphQL';

/**
* @var string
*/
private $endpoint = 'graphql';

/**
* @var string
*/
private $projectDir = '';

/**
* @var string
*/
private $srcDir = 'src';

/**
* @var string
*/
private $perms = '';

/**
* @param HTTPRequest $request
*/
public function initialise(HTTPRequest $request)
{
$isBrowser = !Director::is_cli();
Schema::invariant(
!$isBrowser,
'This task can only be run from CLI'
);

if ($request->getVar('help')) {
$this->showHelp();
return;
}

$appNamespace = $request->getVar('namespace');

if (!$appNamespace) {
echo "Please provide a base namespace for your app, e.g. \"namespace=App\" or \"namespace=MyVendor\MyProject\".\nFor help, run \"dev/graphql/init help=1\"\n";
return;
}

$this->appNamespace = $appNamespace;

$this->projectDir = ModuleManifest::config()->get('project');

$schemaName = $request->getVar('name');
unclecheese marked this conversation as resolved.
Show resolved Hide resolved
if ($schemaName) {
$this->schemaName = $schemaName;
}

$graphqlConfigDir = $request->getVar('graphqlConfigDir');
if ($graphqlConfigDir) {
$this->graphqlConfigDir = $graphqlConfigDir;
}

$graphqlCodeDir = $request->getVar('graphqlCodeDir');
if ($graphqlCodeDir) {
$this->graphqlCodeDir = $graphqlCodeDir;
}

$endpoint = $request->getVar('endpoint');
if ($endpoint) {
$this->endpoint = $endpoint;
}

$srcDir = $request->getVar('srcDir');
if ($srcDir) {
$this->srcDir = $srcDir;
}

$absProjectDir = Path::join(BASE_PATH, $this->projectDir);
$this->perms = fileperms($absProjectDir);

$this->createGraphQLConfig();
$this->createProjectConfig();
$this->createResolvers();
}

/**
* Creates the graphql schema specific config in _graphql/
*/
private function createGraphQLConfig(): void
{
$absGraphQLDir = Path::join(BASE_PATH, $this->projectDir, $this->graphqlConfigDir);
if (is_dir($absGraphQLDir)) {
echo "Graphql config directory already exists. Skipping." . PHP_EOL;
return;
}

echo "Creating graphql config directory: $this->graphqlConfigDir" . PHP_EOL;
mkdir($absGraphQLDir, $this->perms);
foreach (['models', 'config', 'types', 'queries', 'mutations'] as $file) {
unclecheese marked this conversation as resolved.
Show resolved Hide resolved
touch(Path::join($absGraphQLDir, "$file.yml"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think creating a bunch of empty files adds much value. I think it's fine having only the config.yml file here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think it does. There's a lot of naming conventions to learn in GraphQL 4, and this just saves a trip to the docs. At a bare minimum, we need config.yml and models.yml, but really, I'd prefer to just do empty files. You're going to need them at some point, and for users who don't know about custom types/queries, it creates a nice footprint that shows you a bit of what the module can do without forcing you down the documentation path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having worked with GQL4 I tend to agree that any help in code/prebaked configs is closer to the hands-on dev and therefore better than having to go and read docs to then do the same thing anyway. Even from the perspective of naming the files and what they may contain.

}
$configPath = Path::join($absGraphQLDir, 'config.yml');
$defaultConfig = <<<YAML
resolvers:
- $this->appNamespace\Resolvers
YAML;
file_put_contents($configPath, $defaultConfig);
}

/**
* Creates the SS config in _config/graphql.yml
*/
private function createProjectConfig(): void
{
$absConfigFile = Path::join(BASE_PATH, $this->projectDir, '_config', 'graphql.yml');
if (file_exists($absConfigFile)) {
echo "Config file $absConfigFile already exists. Skipping." . PHP_EOL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to suffix the config if the graphql.yml file already exists?

Copy link
Author

@unclecheese unclecheese Jan 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, this would do a similar check that the GraphQL devtools controller does, where it finds what schemas are actually routed. That's in a separate module, and it's got some GraphQL 3 backward compat stuff in it. I don't really have much of an appetite for migrating it into GraphQL 4 right now, but we could at some point. Would probably make sense to put in Controller::getRoutedSchemas() or something.

I'm thinking that if you have a graphql.yml file, you're past the point of needing this tool? I'd rather be paranoid of clobbering someone's existing config.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good way of handling it - if the file already exists, we inform the developer of that fact and skip it. They can then make a judgement call to either keep the existing schema definition or delete it and run this again.

return;
}
$defaultProjectConfig = <<<YAML
SilverStripe\Control\Director:
rules:
$this->endpoint: '%\$SilverStripe\GraphQL\Controller.$this->schemaName'
SilverStripe\GraphQL\Schema\Schema:
schemas:
$this->schemaName:
src:
- $this->projectDir/$this->graphqlConfigDir

YAML;
unclecheese marked this conversation as resolved.
Show resolved Hide resolved
file_put_contents($absConfigFile, $defaultProjectConfig);
}

/**
* Creates an example resolvers class for autodiscovery in app/src/GraphQL/Resolvers.php
*/
private function createResolvers(): void
{
$absSrcDir = Path::join(BASE_PATH, $this->projectDir, $this->srcDir);
$absGraphQLCodeDir = Path::join($absSrcDir, $this->graphqlCodeDir);
$graphqlNamespace = $this->appNamespace . '\\' . str_replace('/', '\\', $this->graphqlCodeDir);
if (is_dir($absGraphQLCodeDir)) {
echo "GraphQL code dir $this->graphqlCodeDir already exists. Skipping" . PHP_EOL;
return;
}

echo "Creating resolvers class in $graphqlNamespace" . PHP_EOL;
mkdir($absGraphQLCodeDir, $this->perms, true);
$resolverFile = Path::join($absGraphQLCodeDir, 'Resolvers.php');
$resolverCode = <<<PHP
<?php

namespace $graphqlNamespace;

/**
* Use this class to define custom resolvers. Static functions in this class
* matching the pattern resolve<FieldName> or resolve<TypeNameFieldName>
* will be automatically assigned to their respective fields.
*
* More information: https://docs.silverstripe.org/en/4/developer_guides/graphql/working_with_generic_types/resolver_discovery/#the-resolver-discovery-pattern
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines too long, will fail PHPCS

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link itself is enough to break the limit. What would you recommend?

*/
class Resolvers
{
public static function resolveMyQuery(\$obj, array \$args, array \$context): array
{
// Return the result of query { myQuery { ... } }
return [];
}
}

PHP;
file_put_contents($resolverFile, $resolverCode);
}

/**
* Outputs help text to the console
*/
private function showHelp(): void
{
echo <<<TXT

****
This task executes a lot of the boilerplate required to build a new GraphQL schema. It will
generate a few files in your project directory. Any files that already exist will not be
overwritten. The task can be run multiple times and is non-destructive.
****

-- Example:

$ vendor/bin/sake dev/graphql/init namespace="MyAgency\MyApp"

-- Arguments:

[namespace]: The root namespace. Required.

<name>: The name of the schema. Default: "default"

<graphqlConfigDir>: The folder where the flushless graphql config files will go. Default: "_graphql"

<graphqlCodeDir>: The subfolder of src/ where your GraphQL code (the resolver class) will go. Follows PSR-4 based on the namespace argument (default: "GraphQL")

TXT;
}
}